/*-------------------------------------------------------------------------
 *
 * nbtpage.c
 *	  Postgres btree 访问方法的 BTree 特定页面管理代码。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/access/nbtree/nbtpage.c
 *
 *	NOTES
 *	   Postgres btree 页面看起来像普通的关系页面。高地址处的不透明数据包括指向左侧和右侧兄弟节点的指针，以及描述页面状态的标志数据。 Btree 中的第一页（页面零）特殊——它存储描述树的元信息。页面一及以上存储实际的树数据。
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/nbtree.h"
#include "access/nbtxlog.h"
#include "access/tableam.h"
#include "access/transam.h"
#include "access/xlog.h"
#include "access/xloginsert.h"
#include "miscadmin.h"
#include "storage/indexfsm.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
#include "storage/procarray.h"
#include "utils/memdebug.h"
#include "utils/memutils.h"
#include "utils/snapmgr.h"

static BTMetaPageData *fc__bt_getmeta(Relation fc_rel, Buffer fc_metabuf);
static void fc__bt_log_reuse_page(Relation fc_rel, BlockNumber fc_blkno,
							   FullTransactionId fc_safexid);
static void fc__bt_delitems_delete(Relation fc_rel, Buffer fc_buf,
								TransactionId fc_latestRemovedXid,
								OffsetNumber *fc_deletable, int fc_ndeletable,
								BTVacuumPosting *fc_updatable, int fc_nupdatable);
static char *fc__bt_delitems_update(BTVacuumPosting *fc_updatable, int fc_nupdatable,
								 OffsetNumber *fc_updatedoffsets,
								 Size *fc_updatedbuflen, bool fc_needswal);
static bool fc__bt_mark_page_halfdead(Relation fc_rel, Buffer fc_leafbuf,
								   BTStack fc_stack);
static bool fc__bt_unlink_halfdead_page(Relation fc_rel, Buffer fc_leafbuf,
									 BlockNumber fc_scanblkno,
									 bool *fc_rightsib_empty,
									 BTVacState *fc_vstate);
static bool fc__bt_lock_subtree_parent(Relation fc_rel, BlockNumber fc_child,
									BTStack fc_stack,
									Buffer *fc_subtreeparent,
									OffsetNumber *fc_poffset,
									BlockNumber *fc_topparent,
									BlockNumber *fc_topparentrightsib);
static void fc__bt_pendingfsm_add(BTVacState *fc_vstate, BlockNumber fc_target,
							   FullTransactionId fc_safexid);

/*
 *	_bt_initmetapage() -- 用正确的元数据页面图像填充页面缓冲区
 */
void _bt_initmetapage(Page fc_page, BlockNumber fc_rootbknum, uint32 fc_level,
				 bool fc_allequalimage)
{
	BTMetaPageData *fc_metad;
	BTPageOpaque fc_metaopaque;

	_bt_pageinit(fc_page, BLCKSZ);

	fc_metad = BTPageGetMeta(fc_page);
	fc_metad->btm_magic = BTREE_MAGIC;
	fc_metad->btm_version = BTREE_VERSION;
	fc_metad->btm_root = fc_rootbknum;
	fc_metad->btm_level = fc_level;
	fc_metad->btm_fastroot = fc_rootbknum;
	fc_metad->btm_fastlevel = fc_level;
	fc_metad->btm_last_cleanup_num_delpages = 0;
	fc_metad->btm_last_cleanup_num_heap_tuples = -1.0;
	fc_metad->btm_allequalimage = fc_allequalimage;

	fc_metaopaque = BTPageGetOpaque(fc_page);
	fc_metaopaque->btpo_flags = BTP_META;

	/*
	 * 将 pd_lower 设置在元数据结束的后面。这是至关重要的，因为不这样做，如果 xlog.c 压缩页面，元数据将丢失。
	 */
	((PageHeader) fc_page)->pd_lower =
		((char *) fc_metad + sizeof(BTMetaPageData)) - (char *) fc_page;
}

/*
 *	_bt_upgrademetapage() -- 将元页面从旧格式升级到版本
 *		3，这是最后一个可以在不广泛影响
 *		磁盘兼容性的情况下进行更新的版本。 （升级到 v4 需要 REINDEX。）
 *
 *		此例程仅执行内存中的图像升级。 调用者
 *		负责锁定、WAL 记录等。
 */
void _bt_upgrademetapage(Page fc_page)
{
	BTMetaPageData *fc_metad;
	BTPageOpaque fc_metaopaque PG_USED_FOR_ASSERTS_ONLY;

	fc_metad = BTPageGetMeta(fc_page);
	fc_metaopaque = BTPageGetOpaque(fc_page);

	/* 它必须是一个可升级版本的元页面 */
	Assert(fc_metaopaque->btpo_flags & BTP_META);
	Assert(fc_metad->btm_version < BTREE_NOVAC_VERSION);
	Assert(fc_metad->btm_version >= BTREE_MIN_VERSION);

	/* 设置版本号并填充版本 3 中添加的额外字段 */
	fc_metad->btm_version = BTREE_NOVAC_VERSION;
	fc_metad->btm_last_cleanup_num_delpages = 0;
	fc_metad->btm_last_cleanup_num_heap_tuples = -1.0;
	/* 只有 REINDEX 才能设置此字段 */
	Assert(!fc_metad->btm_allequalimage);
	fc_metad->btm_allequalimage = false;

	/* 调节 pd_lower (详细信息见 _bt_initmetapage()) */
	((PageHeader) fc_page)->pd_lower =
		((char *) fc_metad + sizeof(BTMetaPageData)) - (char *) fc_page;
}

/*
 * 从共享锁定的包含元页面的缓冲区获取元数据，同时执行
 * 标准完整性检查。
 *
 * 在此处缓存返回数据的调用者应注意，使用 _bt_upgrademetapage() 的动态升级
 * 可以在未使本地缓存失效的情况下更改版本字段
 * 和 BTREE_NOVAC_VERSION 特定字段。
 */
static BTMetaPageData *
fc__bt_getmeta(Relation fc_rel, Buffer fc_metabuf)
{
	Page		fc_metapg;
	BTPageOpaque fc_metaopaque;
	BTMetaPageData *fc_metad;

	fc_metapg = BufferGetPage(fc_metabuf);
	fc_metaopaque = BTPageGetOpaque(fc_metapg);
	fc_metad = BTPageGetMeta(fc_metapg);

	/* 对元页面进行完整性检查 */
	if (!P_ISMETA(fc_metaopaque) ||
		fc_metad->btm_magic != BTREE_MAGIC)
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg("index \"%s\" is not a btree",
						RelationGetRelationName(fc_rel))));

	if (fc_metad->btm_version < BTREE_MIN_VERSION ||
		fc_metad->btm_version > BTREE_VERSION)
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg("version mismatch in index \"%s\": file version %d, "
						"current version %d, minimal supported version %d",
						RelationGetRelationName(fc_rel),
						fc_metad->btm_version, BTREE_VERSION, BTREE_MIN_VERSION)));

	return fc_metad;
}

/*
 * _bt_vacuum_needs_cleanup() -- 检查索引是否需要清理
 *
 * 在 btbulkdelete 从未被调用的情况下，由 btvacuumcleanup 调用，因为没有
 * 需要被删除的索引元组。
 */
bool _bt_vacuum_needs_cleanup(Relation fc_rel)
{
	Buffer		fc_metabuf;
	Page		fc_metapg;
	BTMetaPageData *fc_metad;
	uint32		fc_btm_version;
	BlockNumber fc_prev_num_delpages;

	/*
	 * 快速从元页面复制详细信息到本地变量。
	 *
	 * 请注意，我们故意避免在这里使用元页面的缓存版本。
	 */
	fc_metabuf = _bt_getbuf(fc_rel, BTREE_METAPAGE, BT_READ);
	fc_metapg = BufferGetPage(fc_metabuf);
	fc_metad = BTPageGetMeta(fc_metapg);
	fc_btm_version = fc_metad->btm_version;

	if (fc_btm_version < BTREE_NOVAC_VERSION)
	{
		/*
		 * 元页面需要动态升级以存储仅在 btm_version >= BTREE_NOVAC_VERSION 时
		 * 存在的字段
		 */
		_bt_relbuf(fc_rel, fc_metabuf);
		return true;
	}

	fc_prev_num_delpages = fc_metad->btm_last_cleanup_num_delpages;
	_bt_relbuf(fc_rel, fc_metabuf);

	/*
	 * 在极少数情况下触发清理，当 prev_num_delpages 超过索引总大小的 5% 时。
	 * 如果我们决定在正在进行的 btvacuumcleanup 期间进行 btvacuumscan 调用，
	 * 我们可以合理地期望（尽管不保证）能够回收这么多页面。
	 * 有关更多详细信息，请参阅有关在 FSM 中放置已删除页面的 nbtree/README 部分。
	 */
	if (fc_prev_num_delpages > 0 &&
		fc_prev_num_delpages > RelationGetNumberOfBlocks(fc_rel) / 20)
		return true;

	return false;
}

/*
 * _bt_set_cleanup_info() -- 更新 btvacuumcleanup 的元页面。
 *
 * 在 btvacuumcleanup 结束时调用，此时 num_delpages 值已确定。
 */
void _bt_set_cleanup_info(Relation fc_rel, BlockNumber fc_num_delpages)
{
	Buffer		fc_metabuf;
	Page		fc_metapg;
	BTMetaPageData *fc_metad;

	/*
	 * 磁盘兼容性说明：btm_last_cleanup_num_delpages 元数据页字段最开始是一个名为 btm_oldest_btpo_xact 的 TransactionId 字段。
	 * 这两个“版本”都是 uint32 字段。我们开始在已删除页面中使用 64 位 XIDs 时，重新利用该字段是很方便的。
	 *
	 * 可能 pg_upgrade 的数据库在现在被认为是元数据页的 btm_last_cleanup_num_delpages 字段中会包含一个 XID 值。
	 * _bt_vacuum_needs_cleanup() 甚至可能认为这个值表明需要回收很多页面，而实际上只有一到两个页面。
	 * 最糟糕的情况是会稍早调用 btvacuumscan，这将在我们被调用时将 btm_last_cleanup_num_delpages 设置为一个合理的值。
	 *
	 * 请注意，自 PostgreSQL 14 以来，元数据页的 btm_last_cleanup_num_heap_tuples 字段不再使用。
	 * 我们在重写时将其设置为 -1.0，以保持一致性。
	 */
	fc_metabuf = _bt_getbuf(fc_rel, BTREE_METAPAGE, BT_READ);
	fc_metapg = BufferGetPage(fc_metabuf);
	fc_metad = BTPageGetMeta(fc_metapg);

	/* 不要错过在 BTREE_MIN_VERSION 时升级索引/元数据页的机会 */
	if (fc_metad->btm_version >= BTREE_NOVAC_VERSION &&
		fc_metad->btm_last_cleanup_num_delpages == fc_num_delpages)
	{
		/* 通常意味着索引的 num_delpages 仍为 0 */
		_bt_relbuf(fc_rel, fc_metabuf);
		return;
	}

	/* 用写锁交换我们的读锁 */
	_bt_unlockbuf(fc_rel, fc_metabuf);
	_bt_lockbuf(fc_rel, fc_metabuf, BT_WRITE);

	START_CRIT_SECTION();

	/* 如果需要，升级元页面 */
	if (fc_metad->btm_version < BTREE_NOVAC_VERSION)
		_bt_upgrademetapage(fc_metapg);

	/* 更新与清理相关的信息 */
	fc_metad->btm_last_cleanup_num_delpages = fc_num_delpages;
	fc_metad->btm_last_cleanup_num_heap_tuples = -1.0;
	MarkBufferDirty(fc_metabuf);

	/* 如有必要，写入 WAL 记录 */
	if (RelationNeedsWAL(fc_rel))
	{
		xl_btree_metadata fc_md;
		XLogRecPtr	fc_recptr;

		XLogBeginInsert();
		XLogRegisterBuffer(0, fc_metabuf, REGBUF_WILL_INIT | REGBUF_STANDARD);

		Assert(fc_metad->btm_version >= BTREE_NOVAC_VERSION);
		fc_md.version = fc_metad->btm_version;
		fc_md.root = fc_metad->btm_root;
		fc_md.level = fc_metad->btm_level;
		fc_md.fastroot = fc_metad->btm_fastroot;
		fc_md.fastlevel = fc_metad->btm_fastlevel;
		fc_md.last_cleanup_num_delpages = fc_num_delpages;
		fc_md.allequalimage = fc_metad->btm_allequalimage;

		XLogRegisterBufData(0, (char *) &fc_md, sizeof(xl_btree_metadata));

		fc_recptr = XLogInsert(RM_BTREE_ID, XLOG_BTREE_META_CLEANUP);

		PageSetLSN(fc_metapg, fc_recptr);
	}

	END_CRIT_SECTION();

	_bt_relbuf(fc_rel, fc_metabuf);
}

/*
 *	_bt_getroot() -- 获取 btree 的根页面。
 *
 *		由于根页面可能在 btree 文件中移动，我们必须从元数据页读取它的位置，
 *		然后读取根页面本身。如果根页面尚不存在，我们必须创建一个。
 *
 *		访问类型参数（BT_READ 或 BT_WRITE）控制是否会创建新的根页面。
 *		如果 access = BT_READ，且根页面不存在，我们只返回 InvalidBuffer。
 *		对于 BT_WRITE，如果根页面不存在，我们尝试创建根页面。
 *		注意，返回的根页面即使 access = BT_WRITE 也只会设置一个读锁！
 *
 *		返回的页面不一定是真正的根——它可能是一个“快速根”（由于删除而单独存在于其层级中的页面）。
 *		同样，如果在我们“飞向”根页面时根页面被拆分，
 *		我们将返回的是旧根，而现在只是一个在可能不太宽的层级上的最左边页面。
 *		对于大多数目的而言，这与真正的根一样好或更好，因此我们不必坚持寻找真正的根。
 *		然而，我们保证返回一个存活的（未删除或半死的）页面。
 *
 *		成功返回后，根页面被固定并读锁定。
 *		元数据页在退出时不会被锁定或固定。
 */
Buffer _bt_getroot(Relation fc_rel, int fc_access)
{
	Buffer		fc_metabuf;
	Buffer		fc_rootbuf;
	Page		fc_rootpage;
	BTPageOpaque fc_rootopaque;
	BlockNumber fc_rootblkno;
	uint32		fc_rootlevel;
	BTMetaPageData *fc_metad;

	/*
	 * 尝试使用先前缓存的元数据页数据以查找根。 这通常会为每次索引搜索节省一个缓冲区访问，这对于减少 bufmgr 流量及其争用非常有帮助。
	 */
	if (fc_rel->rd_amcache != NULL)
	{
		fc_metad = (BTMetaPageData *) fc_rel->rd_amcache;
		/* 如果这些失败，我们就不应该缓存它 */
		Assert(fc_metad->btm_magic == BTREE_MAGIC);
		Assert(fc_metad->btm_version >= BTREE_MIN_VERSION);
		Assert(fc_metad->btm_version <= BTREE_VERSION);
		Assert(!fc_metad->btm_allequalimage ||
			   fc_metad->btm_version > BTREE_NOVAC_VERSION);
		Assert(fc_metad->btm_root != P_NONE);

		fc_rootblkno = fc_metad->btm_fastroot;
		Assert(fc_rootblkno != P_NONE);
		fc_rootlevel = fc_metad->btm_fastlevel;

		fc_rootbuf = _bt_getbuf(fc_rel, fc_rootblkno, BT_READ);
		fc_rootpage = BufferGetPage(fc_rootbuf);
		fc_rootopaque = BTPageGetOpaque(fc_rootpage);

		/*
		 * 由于缓存可能已经过时，我们在这里比正常情况更仔细地检查页面。
		 * 我们*必须*检查它是否未被删除。如果它在其层级中不是独占的，那么我们也会拒绝——这可能过于谨慎，但是小心点总比后悔好。
		 * 注意，我们不检查 P_ISROOT，因为在“快速根”中没有设置。
		 */
		if (!P_IGNORE(fc_rootopaque) &&
			fc_rootopaque->btpo_level == fc_rootlevel &&
			P_LEFTMOST(fc_rootopaque) &&
			P_RIGHTMOST(fc_rootopaque))
		{
			/* 好的，将缓存页面作为根接受 */
			return fc_rootbuf;
		}
		_bt_relbuf(fc_rel, fc_rootbuf);
		/* 缓存已经过时，抛弃它 */
		if (fc_rel->rd_amcache)
			pfree(fc_rel->rd_amcache);
		fc_rel->rd_amcache = NULL;
	}

	fc_metabuf = _bt_getbuf(fc_rel, BTREE_METAPAGE, BT_READ);
	fc_metad = fc__bt_getmeta(fc_rel, fc_metabuf);

	/* 如果还没有初始化根页面，则进行初始化 */
	if (fc_metad->btm_root == P_NONE)
	{
		Page		fc_metapg;

		/* 如果访问 = BT_READ，调用者不希望我们立即创建根 */
		if (fc_access == BT_READ)
		{
			_bt_relbuf(fc_rel, fc_metabuf);
			return InvalidBuffer;
		}

		/* 用写锁交换我们的读锁 */
		_bt_unlockbuf(fc_rel, fc_metabuf);
		_bt_lockbuf(fc_rel, fc_metabuf, BT_WRITE);

		/*
		 * 竞争条件：如果在我们释放读取锁和获取写入锁之间，有其他人初始化了元数据，
		 * 我们必须避免再次进行此操作。
		 */
		if (fc_metad->btm_root != P_NONE)
		{
			/*
			 * 元数据由其他人初始化。为了保证没有死锁，
			 * 我们必须释放元数据页面并重新开始。 （这是真的吗？但几乎不值得尝试
			 * 优化这种情况。）
			 */
			_bt_relbuf(fc_rel, fc_metabuf);
			return _bt_getroot(fc_rel, fc_access);
		}

		/*
		 * 获取、初始化、写入，并在新的根页面上留下适当类型的锁。
		 * 由于这是树中的第一页，它既是叶子也是根。
		 */
		fc_rootbuf = _bt_getbuf(fc_rel, P_NEW, BT_WRITE);
		fc_rootblkno = BufferGetBlockNumber(fc_rootbuf);
		fc_rootpage = BufferGetPage(fc_rootbuf);
		fc_rootopaque = BTPageGetOpaque(fc_rootpage);
		fc_rootopaque->btpo_prev = fc_rootopaque->btpo_next = P_NONE;
		fc_rootopaque->btpo_flags = (BTP_LEAF | BTP_ROOT);
		fc_rootopaque->btpo_level = 0;
		fc_rootopaque->btpo_cycleid = 0;
		/* 获取元页面的原始页面指针 */
		fc_metapg = BufferGetPage(fc_metabuf);

		/* 在元数据更新之前不执行 ELOG(ERROR) */
		START_CRIT_SECTION();

		/* 根据需要升级元数据页面 */
		if (fc_metad->btm_version < BTREE_NOVAC_VERSION)
			_bt_upgrademetapage(fc_metapg);

		fc_metad->btm_root = fc_rootblkno;
		fc_metad->btm_level = 0;
		fc_metad->btm_fastroot = fc_rootblkno;
		fc_metad->btm_fastlevel = 0;
		fc_metad->btm_last_cleanup_num_delpages = 0;
		fc_metad->btm_last_cleanup_num_heap_tuples = -1.0;

		MarkBufferDirty(fc_rootbuf);
		MarkBufferDirty(fc_metabuf);

		/* XLOG 相关内容 */
		if (RelationNeedsWAL(fc_rel))
		{
			xl_btree_newroot fc_xlrec;
			XLogRecPtr	fc_recptr;
			xl_btree_metadata fc_md;

			XLogBeginInsert();
			XLogRegisterBuffer(0, fc_rootbuf, REGBUF_WILL_INIT);
			XLogRegisterBuffer(2, fc_metabuf, REGBUF_WILL_INIT | REGBUF_STANDARD);

			Assert(fc_metad->btm_version >= BTREE_NOVAC_VERSION);
			fc_md.version = fc_metad->btm_version;
			fc_md.root = fc_rootblkno;
			fc_md.level = 0;
			fc_md.fastroot = fc_rootblkno;
			fc_md.fastlevel = 0;
			fc_md.last_cleanup_num_delpages = 0;
			fc_md.allequalimage = fc_metad->btm_allequalimage;

			XLogRegisterBufData(2, (char *) &fc_md, sizeof(xl_btree_metadata));

			fc_xlrec.rootblk = fc_rootblkno;
			fc_xlrec.level = 0;

			XLogRegisterData((char *) &fc_xlrec, SizeOfBtreeNewroot);

			fc_recptr = XLogInsert(RM_BTREE_ID, XLOG_BTREE_NEWROOT);

			PageSetLSN(fc_rootpage, fc_recptr);
			PageSetLSN(fc_metapg, fc_recptr);
		}

		END_CRIT_SECTION();

		/*
		 * 将根写锁替换为读锁。在新根页面未上锁时，其他人无法访问它，
		 * 因为还没有人知道它的位置。
		 */
		_bt_unlockbuf(fc_rel, fc_rootbuf);
		_bt_lockbuf(fc_rel, fc_rootbuf, BT_READ);

		/* 好的，元数据是正确的，释放锁而不进行缓存 */
		_bt_relbuf(fc_rel, fc_metabuf);
	}
	else
	{
		fc_rootblkno = fc_metad->btm_fastroot;
		Assert(fc_rootblkno != P_NONE);
		fc_rootlevel = fc_metad->btm_fastlevel;

		/*
		 * 为下次缓存元页面数据
		 */
		fc_rel->rd_amcache = MemoryContextAlloc(fc_rel->rd_indexcxt,
											 sizeof(BTMetaPageData));
		memcpy(fc_rel->rd_amcache, fc_metad, sizeof(BTMetaPageData));

		/*
		 * 我们完成了元页面的处理；安排通过第一次 _bt_relandgetbuf 调用释放它
		 */
		fc_rootbuf = fc_metabuf;

		for (;;)
		{
			fc_rootbuf = _bt_relandgetbuf(fc_rel, fc_rootbuf, fc_rootblkno, BT_READ);
			fc_rootpage = BufferGetPage(fc_rootbuf);
			fc_rootopaque = BTPageGetOpaque(fc_rootpage);

			if (!P_IGNORE(fc_rootopaque))
				break;

			/* 它死了，吉姆。 向右移动一页 */
			if (P_RIGHTMOST(fc_rootopaque))
				elog(ERROR, "no live root page found in index \"%s\"",
					 RelationGetRelationName(fc_rel));
			fc_rootblkno = fc_rootopaque->btpo_next;
		}

		if (fc_rootopaque->btpo_level != fc_rootlevel)
			elog(ERROR, "root page %u of index \"%s\" has level %u, expected %u",
				 fc_rootblkno, RelationGetRelationName(fc_rel),
				 fc_rootopaque->btpo_level, fc_rootlevel);
	}

	/*
	 * 到这里，我们在根页面上都有一个捆绑和读取锁，并且元数据页面上没有设置锁。
	 * 返回根页面的缓冲区。
	 */
	return fc_rootbuf;
}

/*
 *	_bt_gettrueroot() -- 获取 btree 的真实根页面。
 *
 *		这与 _bt_getroot() 的 BT_READ 情况是相同的，只是
 *		我们跟随的是真实根链接而不是快速根链接。
 *
 * 在我们获取根页面的锁时，它可能已经被分裂，并且不再是实际根。
 * 对于该例程的当前用途，这没问题；我们只需要能够向上移动至少一个树级别
 * 从我们所处的任何非根页面。如果我们确实需要锁定那个真实的根页面，
 * 我们可以在这里循环，在每次失败时重新读取元页面。 （请注意，在移动到根页面时保持元页面的锁是不合理的——
 * 这将导致与任何并发的根分裂死锁。）
 */
Buffer _bt_gettrueroot(Relation fc_rel)
{
	Buffer		fc_metabuf;
	Page		fc_metapg;
	BTPageOpaque fc_metaopaque;
	Buffer		fc_rootbuf;
	Page		fc_rootpage;
	BTPageOpaque fc_rootopaque;
	BlockNumber fc_rootblkno;
	uint32		fc_rootlevel;
	BTMetaPageData *fc_metad;

	/*
	 * 我们不尝试在这里使用缓存的元页数据，因为（a）这个路径不是性能关键，且（b）如果我们在这里，则表明我们的缓存过时了。考虑到（b），主动刷新任何缓存的元页信息可能是最安全的做法。
	 */
	if (fc_rel->rd_amcache)
		pfree(fc_rel->rd_amcache);
	fc_rel->rd_amcache = NULL;

	fc_metabuf = _bt_getbuf(fc_rel, BTREE_METAPAGE, BT_READ);
	fc_metapg = BufferGetPage(fc_metabuf);
	fc_metaopaque = BTPageGetOpaque(fc_metapg);
	fc_metad = BTPageGetMeta(fc_metapg);

	if (!P_ISMETA(fc_metaopaque) ||
		fc_metad->btm_magic != BTREE_MAGIC)
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg("index \"%s\" is not a btree",
						RelationGetRelationName(fc_rel))));

	if (fc_metad->btm_version < BTREE_MIN_VERSION ||
		fc_metad->btm_version > BTREE_VERSION)
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg("version mismatch in index \"%s\": file version %d, "
						"current version %d, minimal supported version %d",
						RelationGetRelationName(fc_rel),
						fc_metad->btm_version, BTREE_VERSION, BTREE_MIN_VERSION)));

	/* 如果尚未初始化根页，则失败 */
	if (fc_metad->btm_root == P_NONE)
	{
		_bt_relbuf(fc_rel, fc_metabuf);
		return InvalidBuffer;
	}

	fc_rootblkno = fc_metad->btm_root;
	fc_rootlevel = fc_metad->btm_level;

	/*
	 * 我们已经完成了元页的处理；安排通过第一次 _bt_relandgetbuf 调用来释放它
	 */
	fc_rootbuf = fc_metabuf;

	for (;;)
	{
		fc_rootbuf = _bt_relandgetbuf(fc_rel, fc_rootbuf, fc_rootblkno, BT_READ);
		fc_rootpage = BufferGetPage(fc_rootbuf);
		fc_rootopaque = BTPageGetOpaque(fc_rootpage);

		if (!P_IGNORE(fc_rootopaque))
			break;

		/* 它死了，吉姆。 向右移动一页 */
		if (P_RIGHTMOST(fc_rootopaque))
			elog(ERROR, "no live root page found in index \"%s\"",
				 RelationGetRelationName(fc_rel));
		fc_rootblkno = fc_rootopaque->btpo_next;
	}

	if (fc_rootopaque->btpo_level != fc_rootlevel)
		elog(ERROR, "root page %u of index \"%s\" has level %u, expected %u",
			 fc_rootblkno, RelationGetRelationName(fc_rel),
			 fc_rootopaque->btpo_level, fc_rootlevel);

	return fc_rootbuf;
}

/*
 *	_bt_getrootheight() -- 获取 B-Tree 搜索树的高度。
 *
 *		我们返回当前快速根的级别（从零开始计数）。这代表了我们需要下降的树级数，以开始任何 B-Tree 索引搜索。
 *
 *		这被规划器用于成本估算目的。由于这仅仅是估算，所以略微过时的数据是可以接受的，因此我们不担心更新以前缓存的数据。
 */
int _bt_getrootheight(Relation fc_rel)
{
	BTMetaPageData *fc_metad;

	if (fc_rel->rd_amcache == NULL)
	{
		Buffer		fc_metabuf;

		fc_metabuf = _bt_getbuf(fc_rel, BTREE_METAPAGE, BT_READ);
		fc_metad = fc__bt_getmeta(fc_rel, fc_metabuf);

		/*
		 * 如果尚无根页，_bt_getroot() 不期望创建缓存，因此就在这里停止并报告索引高度为零。
		 * （XXX 也许 _bt_getroot() 应该更改以允许这种情况。）
		 */
		if (fc_metad->btm_root == P_NONE)
		{
			_bt_relbuf(fc_rel, fc_metabuf);
			return 0;
		}

		/*
		 * 为下次缓存元页面数据
		 */
		fc_rel->rd_amcache = MemoryContextAlloc(fc_rel->rd_indexcxt,
											 sizeof(BTMetaPageData));
		memcpy(fc_rel->rd_amcache, fc_metad, sizeof(BTMetaPageData));
		_bt_relbuf(fc_rel, fc_metabuf);
	}

	/* 获取缓存页 */
	fc_metad = (BTMetaPageData *) fc_rel->rd_amcache;
	/* 如果这些失败，我们就不应该缓存它 */
	Assert(fc_metad->btm_magic == BTREE_MAGIC);
	Assert(fc_metad->btm_version >= BTREE_MIN_VERSION);
	Assert(fc_metad->btm_version <= BTREE_VERSION);
	Assert(!fc_metad->btm_allequalimage ||
		   fc_metad->btm_version > BTREE_NOVAC_VERSION);
	Assert(fc_metad->btm_fastroot != P_NONE);

	return fc_metad->btm_fastlevel;
}

/*
 *	_bt_metaversion() -- 从元页获取版本/状态信息。
 *
 *		使用来自 B-Tree 元页的数据（可能是本地缓存的版本）设置调用者的 *heapkeyspace 和 *allequalimage 参数。该信息需要存储在插入的扫描关键字中，因此我们提供一个单一函数同时获取这两者。
 *
 *		这用于确定必须使用的规则以下降 B-Tree。版本 4 索引将堆 TID 视为决胜属性。
 *		pg_upgrade 的版本 3 索引需要额外的步骤来保留插入新的 BTScanInsert-wise 重复元组时的合理性能，这些元组已经存在于很多叶子页中充满此类重复。
 *
 *		还设置 allequalimage 字段，该字段指示是否安全应用去重。我们依赖于假设 btm_allequalimage 会在从 Postgres 12 升级的 heapkeyspace 索引上被置为零。
 */
void _bt_metaversion(Relation fc_rel, bool *fc_heapkeyspace, bool *fc_allequalimage)
{
	BTMetaPageData *fc_metad;

	if (fc_rel->rd_amcache == NULL)
	{
		Buffer		fc_metabuf;

		fc_metabuf = _bt_getbuf(fc_rel, BTREE_METAPAGE, BT_READ);
		fc_metad = fc__bt_getmeta(fc_rel, fc_metabuf);

		/*
		 * 如果尚无根页，_bt_getroot() 不期望创建缓存，因此就在这里停止。（XXX 也许 _bt_getroot() 应该更改以允许这种情况。）
		 */
		if (fc_metad->btm_root == P_NONE)
		{
			*fc_heapkeyspace = fc_metad->btm_version > BTREE_NOVAC_VERSION;
			*fc_allequalimage = fc_metad->btm_allequalimage;

			_bt_relbuf(fc_rel, fc_metabuf);
			return;
		}

		/*
		 * 缓存元页数据以便下次使用
		 *
		 * 通过 _bt_upgrademetapage() 执行的即时版本升级可以在不使任何本地缓存失效的情况下更改索引的 nbtree 版本。这是可以的，因为它只能在从版本 2 迁移到版本 3 的时候发生，这两个版本都是 !heapkeyspace 版本。
		 */
		fc_rel->rd_amcache = MemoryContextAlloc(fc_rel->rd_indexcxt,
											 sizeof(BTMetaPageData));
		memcpy(fc_rel->rd_amcache, fc_metad, sizeof(BTMetaPageData));
		_bt_relbuf(fc_rel, fc_metabuf);
	}

	/* 获取缓存页 */
	fc_metad = (BTMetaPageData *) fc_rel->rd_amcache;
	/* 如果这些失败，我们就不应该缓存它 */
	Assert(fc_metad->btm_magic == BTREE_MAGIC);
	Assert(fc_metad->btm_version >= BTREE_MIN_VERSION);
	Assert(fc_metad->btm_version <= BTREE_VERSION);
	Assert(!fc_metad->btm_allequalimage ||
		   fc_metad->btm_version > BTREE_NOVAC_VERSION);
	Assert(fc_metad->btm_fastroot != P_NONE);

	*fc_heapkeyspace = fc_metad->btm_version > BTREE_NOVAC_VERSION;
	*fc_allequalimage = fc_metad->btm_allequalimage;
}

/*
 *	_bt_checkpage() -- 验证新读取的页面是否看起来合理。
 */
void _bt_checkpage(Relation fc_rel, Buffer fc_buf)
{
	Page		fc_page = BufferGetPage(fc_buf);

	/*
	 * ReadBuffer验证每个新读取的页面是否通过
	 * PageHeaderIsValid，这意味着它要么包含一个相当正常的
	 * 页面头，要么全为零。然而，我们必须防范全零
	 * 的情况。
	 */
	if (PageIsNew(fc_page))
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg("index \"%s\" contains unexpected zero page at block %u",
						RelationGetRelationName(fc_rel),
						BufferGetBlockNumber(fc_buf)),
				 errhint("Please REINDEX it.")));

	/*
	 * 另外检查特殊区域是否正常。
	 */
	if (PageGetSpecialSize(fc_page) != MAXALIGN(sizeof(BTPageOpaqueData)))
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg("index \"%s\" contains corrupted page at block %u",
						RelationGetRelationName(fc_rel),
						BufferGetBlockNumber(fc_buf)),
				 errhint("Please REINDEX it.")));
}

/*
 * 记录从FSM重用的页面。
 */
static void fc__bt_log_reuse_page(Relation fc_rel, BlockNumber fc_blkno, FullTransactionId fc_safexid)
{
	xl_btree_reuse_page fc_xlrec_reuse;

	/*
	 * 注意，我们没有将缓冲区与记录注册，因为此操作不会修改页面。此记录仅存在于为热备用提供冲突点。
	 */

	/* XLOG 相关内容 */
	fc_xlrec_reuse.node = fc_rel->rd_node;
	fc_xlrec_reuse.block = fc_blkno;
	fc_xlrec_reuse.latestRemovedFullXid = fc_safexid;

	XLogBeginInsert();
	XLogRegisterData((char *) &fc_xlrec_reuse, SizeOfBtreeReusePage);

	XLogInsert(RM_BTREE_ID, XLOG_BTREE_REUSE_PAGE);
}

/*
 *	_bt_getbuf() -- 通过块编号获取缓冲区以进行读取或写入。
 *
 *		blkno == P_NEW 意味着获取一个未分配的索引页。该页面
 *		将在返回之前进行初始化。
 *
 *		nbtree中的一般规则是，未经持有缓冲区引脚和缓冲区锁，
 *		严禁访问页面。
 *
 *		当这个例程返回时，适当的锁会被设置在请求的缓冲区上，
 *		其引用计数也会增加（即，缓冲区被“锁定并引脚”）。此外，
 *		我们应用_bt_checkpage来检查页面的完整性（除了P_NEW情况），
 *		并执行Valgrind客户端请求，以帮助Valgrind检测
 *		不安全的页面访问。
 *
 *		注意：在nbtree中不允许原始LockBuffer()调用；
 *		所有缓冲区锁请求需要通过诸如_bt_lockbuf()等包装函数进行。
 */
Buffer _bt_getbuf(Relation fc_rel, BlockNumber fc_blkno, int fc_access)
{
	Buffer		fc_buf;

	if (fc_blkno != P_NEW)
	{
		/* 读取关系的现有块 */
		fc_buf = ReadBuffer(fc_rel, fc_blkno);
		_bt_lockbuf(fc_rel, fc_buf, fc_access);
		_bt_checkpage(fc_rel, fc_buf);
	}
	else
	{
		bool		fc_needLock;
		Page		fc_page;

		Assert(fc_access == BT_WRITE);

		/*
		 * 首先查看FSM是否知道任何自由页面。
		 *
		 * 我们不能完全信任FSM的报告；我们必须检查
		 * 页面是否仍然是免费的。（例如，一个已经自由的页面可能
		 * 在上一个VACUUM扫描它和VACUUM进行FSM更新之间的时间内被重新使用。）
		 *
		 * 事实上，情况更糟：我们甚至不能假设对报告的页面
		 * 进行锁定是安全的。如果其他人对其进行了锁定，
		 * 或者更糟糕的是我们自己的调用者对其进行了锁定，
		 * 我们可能会发生死锁。（自己的调用者场景实际上并非不可能。
		 * 考虑一个序列号或时间戳列上的索引。几乎所有的拆分将发生在
		 * 最右侧页面，因此完全可能的是_bt_split会在持有对最近从FSM获取的页面的锁时调用我们。
		 * 与之前的拆分并行运行的VACUUM可能已经将该页面放回FSM。）
		 *
		 * 为了避免这种情况，我们只要求对报告的页面进行条件锁定。
		 * 如果失败，那么说明其他人正在使用该页面，
		 * 我们可以合理地假设它不是免费的。（如果我们恰好错了，
		 * 最糟糕的后果是该页面将在下一个VACUUM之前无法使用，
		 * 这并不是大问题。）
		 */
		for (;;)
		{
			fc_blkno = GetFreeIndexPage(fc_rel);
			if (fc_blkno == InvalidBlockNumber)
				break;
			fc_buf = ReadBuffer(fc_rel, fc_blkno);
			if (_bt_conditionallockbuf(fc_rel, fc_buf))
			{
				fc_page = BufferGetPage(fc_buf);

				/*
				 * 在索引中找到全零的页面是可能的。例如，一个后端可能
				 * 成功地扩展了关系一页，然后在能够为添加页面
				 * 制作WAL条目之前崩溃。如果我们发现了一个零页，
				 * 那么立即回收它。
				 */
				if (PageIsNew(fc_page))
				{
					/* 可以使用页面。初始化并返回它。 */
					_bt_pageinit(fc_page, BufferGetPageSize(fc_buf));
					return fc_buf;
				}

				if (BTPageIsRecyclable(fc_page))
				{
					/*
					 * 如果我们为热备份生成WAL，则创建一个WAL记录，
					 * 允许我们与在备份上运行的查询发生冲突，
					 * 以防它们的快照比safexid值旧。
					 */
					if (XLogStandbyInfoActive() && RelationNeedsWAL(fc_rel))
						fc__bt_log_reuse_page(fc_rel, fc_blkno,
										   BTPageGetDeleteXid(fc_page));

					/* 可以使用页面。重新初始化并返回。 */
					_bt_pageinit(fc_page, BufferGetPageSize(fc_buf));
					return fc_buf;
				}
				elog(DEBUG2, "FSM returned nonrecyclable page");
				_bt_relbuf(fc_rel, fc_buf);
			}
			else
			{
				elog(DEBUG2, "FSM returned nonlockable page");
				/* 无法获取锁，因此只需释放锁定 */
				ReleaseBuffer(fc_buf);
			}
		}

		/*
		 * 通过一页扩展关系。
		 *
		 * 我们必须使用锁定以确保没有其他人同时扩展关系，
		 * 否则我们都会尝试初始化相同的新页面。
		 * 但是，对于新关系或临时关系，我们可以跳过锁定，
		 * 因为没有其他人可以访问它们。
		 */
		fc_needLock = !RELATION_IS_LOCAL(fc_rel);

		if (fc_needLock)
			LockRelationForExtension(fc_rel, ExclusiveLock);

		fc_buf = ReadBuffer(fc_rel, P_NEW);

		/* 在新页面上获取缓冲区锁定 */
		_bt_lockbuf(fc_rel, fc_buf, BT_WRITE);

		/*
		 * 释放文件扩展锁；现在允许其他人进一步扩展关系。
		 * 注意，在我们获取新页面的缓冲区锁之前，
		 * 不能释放此锁，否则我们会面临与btvacuumscan的竞争条件——请参阅其中的注释。
		 */
		if (fc_needLock)
			UnlockRelationForExtension(fc_rel, ExclusiveLock);

		/* 在返回之前初始化新页面 */
		fc_page = BufferGetPage(fc_buf);
		Assert(PageIsNew(fc_page));
		_bt_pageinit(fc_page, BufferGetPageSize(fc_buf));
	}

	/* 引用计数和锁类型是正确的 */
	return fc_buf;
}

/*
 *	_bt_relandgetbuf() -- 释放一个锁定的缓冲区并获取另一个缓冲区。
 *
 * 这相当于_bt_relbuf后跟_bt_getbuf，但blkno可能不是P_NEW。
 * 此外，如果obuf是InvalidBuffer，则简化为仅_bt_getbuf；
 * 允许这种情况简化了一些调用者。
 *
 * 使用这个的最初动机是避免两次对bufmgr的请求，当一次即可。
 * 然而，现在主要只是为了表示方便。
 * 唯一能节省_bt_relbuf/_bt_getbuf工作的情况是当目标页面
 * 与缓冲区中已经存在的页面相同。
 */
Buffer _bt_relandgetbuf(Relation fc_rel, Buffer fc_obuf, BlockNumber fc_blkno, int fc_access)
{
	Buffer		fc_buf;

	Assert(fc_blkno != P_NEW);
	if (BufferIsValid(fc_obuf))
		_bt_unlockbuf(fc_rel, fc_obuf);
	fc_buf = ReleaseAndReadBuffer(fc_obuf, fc_rel, fc_blkno);
	_bt_lockbuf(fc_rel, fc_buf, fc_access);

	_bt_checkpage(fc_rel, fc_buf);
	return fc_buf;
}

/*
 *	_bt_relbuf() -- 释放一个锁定的缓冲区。
 *
 * 锁定和pin（引用计数）都被释放。
 */
void _bt_relbuf(Relation fc_rel, Buffer fc_buf)
{
	_bt_unlockbuf(fc_rel, fc_buf);
	ReleaseBuffer(fc_buf);
}

/*
 *	_bt_lockbuf() -- 锁定一个固定的缓冲区。
 *
 * 在不获取另一个pin的情况下获取锁定。
 * 这类似于原始的LockBuffer()调用，但执行Valgrind所需的额外步骤。
 *
 * 注意：调用方可能需要在pin未在_bt_getbuf()或_bt_relandgetbuf()中最初获取时
 * 调用_bt_checkpage()与buf一起。
 */
void _bt_lockbuf(Relation fc_rel, Buffer fc_buf, int fc_access)
{
	/* LockBuffer()断言该pin被该后端持有 */
	LockBuffer(fc_buf, fc_access);

	/*
	 * _bt_unlockbuf() 不会在 nbtree 错误（例如，唯一性违反错误）的情况下被调用，这并不重要。
	 * 这不会导致 Valgrind 假阳性。
	 *
	 * nbtree 客户端请求叠加在 bufmgr.c 的缓冲区锁定客户端请求之上。
	 * 在发生 nbtree 错误时，当后端再次获取其对缓冲区的第一个锁时，
	 * 缓冲区肯定会被标记为已定义。（当然，如果后端再也不接触缓冲区，
	 * 那么缓冲区仍然对 Valgrind 不可访问也无所谓。）
	 *
	 * 注意：当一个 IndexTuple C 指针使用在持锁状态下从页面读取的 ItemId 计算时，
	 * 一旦锁被释放，该 C 指针将永远变得不安全以解除引用。
	 * 不过，Valgrind 只能检测到在没有 _current_ 锁/锁定持有的情况下解除引用的情况。
	 */
	if (!RelationUsesLocalBuffers(fc_rel))
		VALGRIND_MAKE_MEM_DEFINED(BufferGetPage(buf), BLCKSZ);
}

/*
 *	_bt_unlockbuf() -- 解锁固定的缓冲区。
 */
void _bt_unlockbuf(Relation fc_rel, Buffer fc_buf)
{
	/*
	 * 缓冲区被固定并锁定，这意味着它预计是已定义和可寻址的。
	 * 积极检查这一点。
	 */
	VALGRIND_CHECK_MEM_IS_DEFINED(BufferGetPage(buf), BLCKSZ);

	/* LockBuffer()断言该pin被该后端持有 */
	LockBuffer(fc_buf, BUFFER_LOCK_UNLOCK);

	if (!RelationUsesLocalBuffers(fc_rel))
		VALGRIND_MAKE_MEM_NOACCESS(BufferGetPage(buf), BLCKSZ);
}

/*
 *	_bt_conditionallockbuf() -- 有条件地对固定的缓冲区加 BT_WRITE 锁。
 *
 * 注意：调用者可能需要在 buf 上调用 _bt_checkpage()，
 * 如果 buf 上的锁不会在 _bt_getbuf() 或 _bt_relandgetbuf() 中原始获取。
 */
bool _bt_conditionallockbuf(Relation fc_rel, Buffer fc_buf)
{
	/* ConditionalLockBuffer() 断言该后端持有锁定 */
	if (!ConditionalLockBuffer(fc_buf))
		return false;

	if (!RelationUsesLocalBuffers(fc_rel))
		VALGRIND_MAKE_MEM_DEFINED(BufferGetPage(buf), BLCKSZ);

	return true;
}

/*
 *	_bt_upgradelockbufcleanup() -- 将锁定升级为完整的清理锁。
 */
void _bt_upgradelockbufcleanup(Relation fc_rel, Buffer fc_buf)
{
	/*
	 * 缓冲区被固定并锁定，这意味着它预计是已定义和可寻址的。
	 * 积极检查这一点。
	 */
	VALGRIND_CHECK_MEM_IS_DEFINED(BufferGetPage(buf), BLCKSZ);

	/* LockBuffer()断言该pin被该后端持有 */
	LockBuffer(fc_buf, BUFFER_LOCK_UNLOCK);
	LockBufferForCleanup(fc_buf);
}

/*
 *	_bt_pageinit() -- 初始化一个新页面。
 *
 * 返回时，页面头已经初始化；数据空间是空的；
 * 特殊空间被清零。
 */
void _bt_pageinit(Page fc_page, Size fc_size)
{
	PageInit(fc_page, fc_size, sizeof(BTPageOpaqueData));
}

/*
 * 在 VACUUM 期间从 btree 叶子页面删除项目。
 *
 * 此例程假设调用者已经对缓冲区具有完整的清理锁。
 * 此外，给定的可删除和可更新数组 *必须* 按升序排序。
 *
 * 该例程处理在现有的发布列表项中，某些（但不是全部）堆 TID 需要被移除时删除 TID。
 * 这通过用调用者的新版本项目（缺少待删除的 TID 的版本）更新/覆盖现有项目来实现。
 *
 * 我们在 WAL 中以不同方式记录 VACUUM 和 b-tree 删除。
 * 删除必须通过直接访问表生成其自己的 latestRemovedXid，
 * 而 VACUUM 则依赖于初始 VACUUM 表扫描进行 WAL 日志记录，这间接处理了表索引的问题。
 * 此外，我们从页面中移除 VACUUM 循环 ID，而 b-tree 删除则不这样做。
 */
void _bt_delitems_vacuum(Relation fc_rel, Buffer fc_buf,
					OffsetNumber *fc_deletable, int fc_ndeletable,
					BTVacuumPosting *fc_updatable, int fc_nupdatable)
{
	Page		fc_page = BufferGetPage(fc_buf);
	BTPageOpaque fc_opaque;
	bool		fc_needswal = RelationNeedsWAL(fc_rel);
	char	   *fc_updatedbuf = NULL;
	Size		fc_updatedbuflen = 0;
	OffsetNumber fc_updatedoffsets[MaxIndexTuplesPerPage];

	/* 除非有事情要做，否则不应调用 */
	Assert(fc_ndeletable > 0 || fc_nupdatable > 0);

	/* 生成不包含已删除 TID 的发布列表的新版本 */
	if (fc_nupdatable > 0)
		fc_updatedbuf = fc__bt_delitems_update(fc_updatable, fc_nupdatable,
										 fc_updatedoffsets, &fc_updatedbuflen,
										 fc_needswal);

	/* 直到更改被记录，不要ereport(ERROR) */
	START_CRIT_SECTION();

	/*
	 * 处理元组更新的发布。
	 *
	 * 故意在处理简单删除之前执行此操作。如果我们反过来做
	 * （即 WAL 记录顺序 -- 简单删除在更新之前）那么我们必须对
	 * 可更新的偏移量数组进行补偿更改。
	 *
	 * PageIndexTupleOverwrite() 在每个项目的 LP_DEAD 位已设置的情况下
	 * 不会取消设置它。重要的是我们不干扰未来的简单索引元组删除操作。
	 */
	for (int fc_i = 0; fc_i < fc_nupdatable; fc_i++)
	{
		OffsetNumber fc_updatedoffset = fc_updatedoffsets[fc_i];
		IndexTuple	fc_itup;
		Size		fc_itemsz;

		fc_itup = fc_updatable[fc_i]->itup;
		fc_itemsz = MAXALIGN(IndexTupleSize(fc_itup));
		if (!PageIndexTupleOverwrite(fc_page, fc_updatedoffset, (Item) fc_itup,
									 fc_itemsz))
			elog(PANIC, "failed to update partially dead item in block %u of index \"%s\"",
				 BufferGetBlockNumber(fc_buf), RelationGetRelationName(fc_rel));
	}

	/* 现在处理整个元组的简单删除 */
	if (fc_ndeletable > 0)
		PageIndexMultiDelete(fc_page, fc_deletable, fc_ndeletable);

	/*
	 * 我们可以清除真空周期 ID，因为此页面肯定已被当前的真空扫描处理。
	 */
	fc_opaque = BTPageGetOpaque(fc_page);
	fc_opaque->btpo_cycleid = 0;

	/*
	 * 清除 BTP_HAS_GARBAGE 页面标志。
	 *
	 * 此标志指示页面上存在 LP_DEAD 项（尽管
	 * 并不可靠）。请注意，我们仅在 pg_upgrade 后的
	 * !heapkeyspace 索引中依赖于它。因此在这里清除它通常不会
	 * 干扰简单索引元组删除。
	 */
	fc_opaque->btpo_flags &= ~BTP_HAS_GARBAGE;

	MarkBufferDirty(fc_buf);

	/* XLOG 相关内容 */
	if (fc_needswal)
	{
		XLogRecPtr	fc_recptr;
		xl_btree_vacuum fc_xlrec_vacuum;

		fc_xlrec_vacuum.ndeleted = fc_ndeletable;
		fc_xlrec_vacuum.nupdated = fc_nupdatable;

		XLogBeginInsert();
		XLogRegisterBuffer(0, fc_buf, REGBUF_STANDARD);
		XLogRegisterData((char *) &fc_xlrec_vacuum, SizeOfBtreeVacuum);

		if (fc_ndeletable > 0)
			XLogRegisterBufData(0, (char *) fc_deletable,
								fc_ndeletable * sizeof(OffsetNumber));

		if (fc_nupdatable > 0)
		{
			XLogRegisterBufData(0, (char *) fc_updatedoffsets,
								fc_nupdatable * sizeof(OffsetNumber));
			XLogRegisterBufData(0, fc_updatedbuf, fc_updatedbuflen);
		}

		fc_recptr = XLogInsert(RM_BTREE_ID, XLOG_BTREE_VACUUM);

		PageSetLSN(fc_page, fc_recptr);
	}

	END_CRIT_SECTION();

	/* 这里不能泄漏内存 */
	if (fc_updatedbuf != NULL)
		pfree(fc_updatedbuf);
	/* 释放在 _bt_delitems_update() 中分配的元组 */
	for (int fc_i = 0; fc_i < fc_nupdatable; fc_i++)
		pfree(fc_updatable[fc_i]->itup);
}

/*
 * 在单页清理过程中从 btree 叶子页面删除项。
 *
 * 此例程假定调用者已固定并写锁定缓冲区。此外，给定的可删除和
 * 可更新数组 *必须* 按升序排列。
 *
 * 此例程处理删除在现有发布列表项中存在的堆 TID 中一些（但不是全部）TID。
 * 这通过用调用者的新版本（缺少要删除的 TID）更新/覆盖现有项来实现。
 *
 * 就对页面所做的操作而言，这几乎与 _bt_delitems_vacuum 一样，
 * 但它需要从调用者处获取自己的最新的 removedXid（调用者从 tableam 获得）。
 * 这由 REDO 例程用于生成恢复冲突。另一个不同之处在于，只有 _bt_delitems_vacuum
 * 会清除页面的 VACUUM 周期 ID。
 */
static void fc__bt_delitems_delete(Relation fc_rel, Buffer fc_buf, TransactionId fc_latestRemovedXid,
					OffsetNumber *fc_deletable, int fc_ndeletable,
					BTVacuumPosting *fc_updatable, int fc_nupdatable)
{
	Page		fc_page = BufferGetPage(fc_buf);
	BTPageOpaque fc_opaque;
	bool		fc_needswal = RelationNeedsWAL(fc_rel);
	char	   *fc_updatedbuf = NULL;
	Size		fc_updatedbuflen = 0;
	OffsetNumber fc_updatedoffsets[MaxIndexTuplesPerPage];

	/* 除非有事情要做，否则不应调用 */
	Assert(fc_ndeletable > 0 || fc_nupdatable > 0);

	/* 生成没有删除的 TID 的发布列表的新版本 */
	if (fc_nupdatable > 0)
		fc_updatedbuf = fc__bt_delitems_update(fc_updatable, fc_nupdatable,
										 fc_updatedoffsets, &fc_updatedbuflen,
										 fc_needswal);

	/* 直到更改被记录，不要ereport(ERROR) */
	START_CRIT_SECTION();

	/* 处理更新和删除，与 _bt_delitems_vacuum 一样 */
	for (int fc_i = 0; fc_i < fc_nupdatable; fc_i++)
	{
		OffsetNumber fc_updatedoffset = fc_updatedoffsets[fc_i];
		IndexTuple	fc_itup;
		Size		fc_itemsz;

		fc_itup = fc_updatable[fc_i]->itup;
		fc_itemsz = MAXALIGN(IndexTupleSize(fc_itup));
		if (!PageIndexTupleOverwrite(fc_page, fc_updatedoffset, (Item) fc_itup,
									 fc_itemsz))
			elog(PANIC, "failed to update partially dead item in block %u of index \"%s\"",
				 BufferGetBlockNumber(fc_buf), RelationGetRelationName(fc_rel));
	}

	if (fc_ndeletable > 0)
		PageIndexMultiDelete(fc_page, fc_deletable, fc_ndeletable);

	/*
	 * 与 _bt_delitems_vacuum 不同，我们 *必须不* 在此时清除真空周期 ID。
	 * VACUUM 命令单独控制真空周期 ID。
	 */
	fc_opaque = BTPageGetOpaque(fc_page);

	/*
	 * 清除 BTP_HAS_GARBAGE 页面标志。
	 *
	 * 此标志指示页面上存在 LP_DEAD 项（尽管
	 * 并不可靠）。请注意，我们仅在 pg_upgrade 后的
	 * !heapkeyspace 索引中依赖于它。
	 */
	fc_opaque->btpo_flags &= ~BTP_HAS_GARBAGE;

	MarkBufferDirty(fc_buf);

	/* XLOG 相关内容 */
	if (fc_needswal)
	{
		XLogRecPtr	fc_recptr;
		xl_btree_delete fc_xlrec_delete;

		fc_xlrec_delete.latestRemovedXid = fc_latestRemovedXid;
		fc_xlrec_delete.ndeleted = fc_ndeletable;
		fc_xlrec_delete.nupdated = fc_nupdatable;

		XLogBeginInsert();
		XLogRegisterBuffer(0, fc_buf, REGBUF_STANDARD);
		XLogRegisterData((char *) &fc_xlrec_delete, SizeOfBtreeDelete);

		if (fc_ndeletable > 0)
			XLogRegisterBufData(0, (char *) fc_deletable,
								fc_ndeletable * sizeof(OffsetNumber));

		if (fc_nupdatable > 0)
		{
			XLogRegisterBufData(0, (char *) fc_updatedoffsets,
								fc_nupdatable * sizeof(OffsetNumber));
			XLogRegisterBufData(0, fc_updatedbuf, fc_updatedbuflen);
		}

		fc_recptr = XLogInsert(RM_BTREE_ID, XLOG_BTREE_DELETE);

		PageSetLSN(fc_page, fc_recptr);
	}

	END_CRIT_SECTION();

	/* 这里不能泄漏内存 */
	if (fc_updatedbuf != NULL)
		pfree(fc_updatedbuf);
	/* 释放在 _bt_delitems_update() 中分配的元组 */
	for (int fc_i = 0; fc_i < fc_nupdatable; fc_i++)
		pfree(fc_updatable[fc_i]->itup);
}

/*
 * 设置需要从发布列表元组中删除 TID 所需的状态，通过“更新”
 * 元组。执行 _bt_delitems_vacuum 和
 * _bt_delitems_delete 两者共有的步骤。这些步骤必须在每个函数的
 * 关键区开始之前进行。
 *
 * updatable 和 nupdatable 是输入参数，尽管请注意我们将使用
 * _bt_update_posting() 用指向在 palloc() 中的最终版本的指针替换
 * 原始 itup。调用者在完成时应释放元组。
 *
 * 更新偏移量中的前 nupdatable 条目被设置为调用者更新的发布列表
 * 元组的页面偏移量编号。这主要是因为调用者可能需要 WAL 记录
 * 页面偏移量（尽管出于便利我们总是为调用者执行此操作）。
 *
 * 返回由 xl_btree_update 结构数组组成的缓冲区，描述了我们为
 * 调用者在此执行的步骤（仅在 needswal 为 true 时）。还将
 * *updatedbuflen 设置为缓冲区的最终大小。此缓冲区在需要
 * WAL 记录时由调用者使用。
 */
static char * fc__bt_delitems_update(BTVacuumPosting *fc_updatable, int fc_nupdatable,
					OffsetNumber *fc_updatedoffsets, Size *fc_updatedbuflen,
					bool fc_needswal)
{
	char	   *fc_updatedbuf = NULL;
	Size		fc_buflen = 0;

	/* 除非有事情要做，否则不应调用 */
	Assert(fc_nupdatable > 0);

	for (int fc_i = 0; fc_i < fc_nupdatable; fc_i++)
	{
		BTVacuumPosting fc_vacposting = fc_updatable[fc_i];
		Size		fc_itemsz;

		/* 用更新版本替换工作区 IndexTuple */
		_bt_update_posting(fc_vacposting);

		/* 记录为传递的 updatedbuf 的 xl_btree_update 的大小 */
		fc_itemsz = SizeOfBtreeUpdate + fc_vacposting->ndeletedtids * sizeof(uint16);
		fc_buflen += fc_itemsz;

		/* 在传递中构建 updatedoffsets 缓冲区 */
		fc_updatedoffsets[fc_i] = fc_vacposting->updatedoffset;
	}

	/* XLOG 相关内容 */
	if (fc_needswal)
	{
		Size		fc_offset = 0;

		/* 为调用者分配，设置最终大小 */
		fc_updatedbuf = palloc(fc_buflen);
		*fc_updatedbuflen = fc_buflen;
		for (int fc_i = 0; fc_i < fc_nupdatable; fc_i++)
		{
			BTVacuumPosting fc_vacposting = fc_updatable[fc_i];
			Size		fc_itemsz;
			xl_btree_update fc_update;

			fc_update.ndeletedtids = fc_vacposting->ndeletedtids;
			memcpy(fc_updatedbuf + fc_offset, &fc_update.ndeletedtids,
				   SizeOfBtreeUpdate);
			fc_offset += SizeOfBtreeUpdate;

			fc_itemsz = fc_update.ndeletedtids * sizeof(uint16);
			memcpy(fc_updatedbuf + fc_offset, fc_vacposting->deletetids, fc_itemsz);
			fc_offset += fc_itemsz;
		}
	}

	return fc_updatedbuf;
}

/*
 * 由 _bt_delitems_delete_check() 使用的比较器，用于将 deltids 数组
 * 恢复到其原始的按叶页排序的顺序
 */
static int fc__bt_delitems_cmp(const void *fc_a, const void *fc_b)
{
	TM_IndexDelete *fc_indexdelete1 = (TM_IndexDelete *) fc_a;
	TM_IndexDelete *fc_indexdelete2 = (TM_IndexDelete *) fc_b;

	if (fc_indexdelete1->id > fc_indexdelete2->id)
		return 1;
	if (fc_indexdelete1->id < fc_indexdelete2->id)
		return -1;

	Assert(false);

	return 0;
}

/*
 * 尝试在单页面清理过程中从btree叶页面删除项。
 *
 * nbtree接口用于table_index_delete_tuples()。删除调用者的deltids数组中的一部分索引
 * 元组：那些其TIDs被tableam认为安全删除的元组（或者在索引中已标记为LP_DEAD，因此
 * 已被简单索引删除调用者知道可以删除）。我们实际从buf叶页面最后删除索引元组（对于
 * 已知在调用table_index_delete_tuples()后是安全的索引元组）。
 *
 * 简单索引删除调用者仅包括标记为LP_DEAD的索引元组中的TIDs，以及它在同一叶页面上
 * 找到的可以包含而不会增加整个删除操作的不同表块总数的额外TIDs。这种方法通常
 * 允许我们删除一些对tableam来说在检查时实际上是免费的额外索引元组（当它们实际上
 * 确实被证明是可以安全删除时）。只有当tableam是块导向时，这种额外检查才有意义
 * （否则这些检查可能实际上不会是免费的，这是我们所依赖的）。不过，tableam接口
 * 要求tableam一方来处理这个问题，所以这样做是可以的（作为索引AM，我们可以自由
 * 地简化假设所有tableams必须是基于块的）。
 *
 * 自下而上的索引删除调用者提供所有来自叶页面的TIDs，而不是期望tableam检查其中
 * 大多数。tableam在检查哪些条目/块时有相当大的自由裁量权。在成本评估自下而上的
 * 删除操作中，我们的角色完全是顾问性质的。
 *
 * 注意：调用者必须按叶页面顺序添加deltids条目（即放入delstate的主数组中的条目）
 * ：页面偏移号顺序，来自相同发布列表元组的条目中的TID顺序（TID的平局打破）。
 * 这种顺序在这里便于处理。
 *
 * 注意：我们还依赖每个deltids元素的id字段“捕获”这种原始的叶页面顺序。也就是说，
 * 我们期望能够仅通过按id字段对deltids进行排序来恢复原始的叶页面顺序（tableam
 * 将出于自己的原因对deltids进行排序，因此我们需要在之后将其放回叶页面顺序）。
 */
void _bt_delitems_delete_check(Relation fc_rel, Buffer fc_buf, Relation fc_heapRel,
						  TM_IndexDeleteOp *fc_delstate)
{
	Page		fc_page = BufferGetPage(fc_buf);
	TransactionId fc_latestRemovedXid;
	OffsetNumber fc_postingidxoffnum = InvalidOffsetNumber;
	int			fc_ndeletable = 0,
				fc_nupdatable = 0;
	OffsetNumber fc_deletable[MaxIndexTuplesPerPage];
	BTVacuumPosting fc_updatable[MaxIndexTuplesPerPage];

	/* 使用tableam接口确定首先删除哪些元组 */
	fc_latestRemovedXid = table_index_delete_tuples(fc_heapRel, fc_delstate);

	/* 除非需要，否则不应记录最新的RemovedXid */
	if (!XLogStandbyInfoActive() || !RelationNeedsWAL(fc_rel))
		fc_latestRemovedXid = InvalidTransactionId;

	/*
	 * 构建叶页面顺序的描述以说明_bt_delitems_delete()
	 * 需要做什么以物理删除页面中的索引元组。
	 *
	 * 必须对deltids数组进行排序以恢复叶页面顺序（在对tableam调用之前的原始顺序）。
	 * 这是循环所期望的顺序。
	 *
	 * 请注意，deltids数组现在可能小得多。它甚至可能完全没有条目（与自下而上的删除
	 * 调用者），在这种情况下，没有其他事情可以做。
	 */
	qsort(fc_delstate->deltids, fc_delstate->ndeltids, sizeof(TM_IndexDelete),
		  fc__bt_delitems_cmp);
	if (fc_delstate->ndeltids == 0)
	{
		Assert(fc_delstate->bottomup);
		return;
	}

	/* 我们确实必须至少删除一个索引元组（或一个TID） */
	for (int fc_i = 0; fc_i < fc_delstate->ndeltids; fc_i++)
	{
		TM_IndexStatus *fc_dstatus = fc_delstate->status + fc_delstate->deltids[fc_i].id;
		OffsetNumber fc_idxoffnum = fc_dstatus->idxoffnum;
		ItemId		fc_itemid = PageGetItemId(fc_page, fc_idxoffnum);
		IndexTuple	fc_itup = (IndexTuple) PageGetItem(fc_page, fc_itemid);
		int			fc_nestedi,
					fc_nitem;
		BTVacuumPosting fc_vacposting;

		Assert(OffsetNumberIsValid(fc_idxoffnum));

		if (fc_idxoffnum == fc_postingidxoffnum)
		{
			/*
			 * 此deltid条目是来自已完全处理的发布列表元组的
			 * 一个TID
			 */
			Assert(BTreeTupleIsPosting(fc_itup));
			Assert(ItemPointerCompare(BTreeTupleGetHeapTID(fc_itup),
									  &fc_delstate->deltids[fc_i].tid) < 0);
			Assert(ItemPointerCompare(BTreeTupleGetMaxHeapTID(fc_itup),
									  &fc_delstate->deltids[fc_i].tid) >= 0);
			continue;
		}

		if (!BTreeTupleIsPosting(fc_itup))
		{
			/* 普通非枢轴元组 */
			Assert(ItemPointerEquals(&fc_itup->t_tid, &fc_delstate->deltids[fc_i].tid));
			if (fc_dstatus->knowndeletable)
				fc_deletable[fc_ndeletable++] = fc_idxoffnum;
			continue;
		}

		/*
		 * itup是一个发布列表元组，其最低的deltids条目（可能
		 * 是或不是来自itup的第一个TID）现在在此考虑。
		 * 不过，我们应该现在处理发布列表的所有deltids条目，
		 * 而不仅仅是最低的条目。记住在外层循环的后续迭代中跳过
		 * 之后的itup相关条目。
		 */
		fc_postingidxoffnum = fc_idxoffnum;	/* 记住在外层循环中的工作 */
		fc_nestedi = fc_i;			/* 初始化第一个itup的deltids条目 */
		fc_vacposting = NULL;		/* 描述itup的最终操作 */
		fc_nitem = BTreeTupleGetNPosting(fc_itup);
		for (int fc_p = 0; fc_p < fc_nitem; fc_p++)
		{
			ItemPointer fc_ptid = BTreeTupleGetPostingN(fc_itup, fc_p);
			int			fc_ptidcmp = -1;

			/*
			 * 此嵌套循环重用从itup中获取的ptid TID之间的工作。
			 * 我们利用itup的TID和deltids条目（在单个itup/发布列表分组内）
			 * 都必须按升序排列的事实。
			 */
			for (; fc_nestedi < fc_delstate->ndeltids; fc_nestedi++)
			{
				TM_IndexDelete *fc_tcdeltid = &fc_delstate->deltids[fc_nestedi];
				TM_IndexStatus *fc_tdstatus = (fc_delstate->status + fc_tcdeltid->id);

				/* 一旦我们超过所有与itup相关的deltids条目就停止 */
				Assert(fc_tdstatus->idxoffnum >= fc_idxoffnum);
				if (fc_tdstatus->idxoffnum != fc_idxoffnum)
					break;

				/* 前面跳过不可删除的itup相关条目 */
				if (!fc_tdstatus->knowndeletable)
					continue;

				/* 条目是第一个部分ptid匹配（或精确匹配）? */
				fc_ptidcmp = ItemPointerCompare(&fc_tcdeltid->tid, fc_ptid);
				if (fc_ptidcmp >= 0)
				{
					/* 大于或等于（部分或精确）匹配... */
					break;
				}
			}

			/* ...对可删除的deltids条目的精确ptid匹配? */
			if (fc_ptidcmp != 0)
				continue;

			/* 对可删除的deltids条目的精确匹配 -- ptid被删除 */
			if (fc_vacposting == NULL)
			{
				fc_vacposting = palloc(offsetof(BTVacuumPostingData, deletetids) +
									fc_nitem * sizeof(uint16));
				fc_vacposting->itup = fc_itup;
				fc_vacposting->updatedoffset = fc_idxoffnum;
				fc_vacposting->ndeletedtids = 0;
			}
			fc_vacposting->deletetids[fc_vacposting->ndeletedtids++] = fc_p;
		}

		/* 对itup的最终决策，发布列表元组 */

		if (fc_vacposting == NULL)
		{
			/* 没有要从itup中删除的TID -- 什么也不做 */
		}
		else if (fc_vacposting->ndeletedtids == fc_nitem)
		{
			/* 直接删除itup（以删除所有TID） */
			fc_deletable[fc_ndeletable++] = fc_idxoffnum;
			/* 结果是我们不需要详细信息 */
			pfree(fc_vacposting);
		}
		else
		{
			/* 从itup中删除一些（但不是全部）TID */
			Assert(fc_vacposting->ndeletedtids > 0 &&
				   fc_vacposting->ndeletedtids < fc_nitem);
			fc_updatable[fc_nupdatable++] = fc_vacposting;
		}
	}

	/* 使用可删除（或可更新）物理删除元组（或TID） */
	fc__bt_delitems_delete(fc_rel, fc_buf, fc_latestRemovedXid, fc_deletable, fc_ndeletable,
						fc_updatable, fc_nupdatable);

	/* 保持整洁 */
	for (int fc_i = 0; fc_i < fc_nupdatable; fc_i++)
		pfree(fc_updatable[fc_i]);
}

/*
 * 检查左侧兄弟页面（目标页面的btpo_prev）没有被标记为
 * INCOMPLETE_SPLIT标志。用于页面删除。
 *
 * 返回true表示左侧兄弟中的页面标志已设置（它
 * 绝对仍然是目标的左兄弟）。当发生这种情况时，目标没有
 * 在父级中的下行链接，页面删除算法
 * 无法处理这种情况。目标页面（或包含目标页面的整个
 * 子树）的删除无法进行。
 *
 * 调用者不应对目标页面本身有锁，因为同一层级的页面
 * 必须始终从左到右锁定，以避免死锁。
 */
static bool fc__bt_leftsib_splitflag(Relation fc_rel, BlockNumber fc_leftsib, BlockNumber fc_target)
{
	Buffer		fc_buf;
	Page		fc_page;
	BTPageOpaque fc_opaque;
	bool		fc_result;

	/* 简单案例：没有左兄弟 */
	if (fc_leftsib == P_NONE)
		return false;

	fc_buf = _bt_getbuf(fc_rel, fc_leftsib, BT_READ);
	fc_page = BufferGetPage(fc_buf);
	fc_opaque = BTPageGetOpaque(fc_page);

	/*
	 * 如果左兄弟被并发拆分，以至于它的下一个指针
	 * 不再指向当前页面，则创建目标的拆分必须完成。
	 * 调用者可以合理预期将有一个指向目标页面的下行链接，
	 * 它可以使用其堆栈重新定位。 
	 * （我们不允许对一个未完全拆分的页面再次进行拆分，直到
	 * 先前的拆分已完成。）
	 */
	fc_result = (fc_opaque->btpo_next == fc_target && P_INCOMPLETE_SPLIT(fc_opaque));
	_bt_relbuf(fc_rel, fc_buf);

	return fc_result;
}

/*
 * 检查叶子右兄弟页面（目标叶子页面的 btpo_next）是否没有标记为 ISHALFDEAD 标志。在页面删除过程中使用。
 *
 * 返回 true 表示叶子右兄弟中的页面标志被设置，因此页面删除无法进行。我们的调用者无法处理父页面没有指向叶子右兄弟的枢轴元组的情况（由于之前中断的 VACUUM 操作）。似乎没有必要费心教我们的调用者处理这种情况。在 VACUUM 完成半死页面的删除后，这种情况将得到解决（当未来的 VACUUM 操作再次到达目标页面时）。
 *
 * _bt_leftsib_splitflag() 被用于叶子页面和内部页面。但 _bt_rightsib_halfdeadflag() 仅在叶子页面中调用。这是可以的，因为对删除作为其父页面的最右页面的页面有一定的限制（即这样的删除只能在整个子树必须被删除时进行）。这里进行的叶级检查将在实际调用者尝试删除高于叶子页面的页面时，适用于右 "表亲" 叶子页面，而不是简单的右兄弟叶子页面。右表亲叶子页面代表了将要删除的子树右侧整个子树的左边缘，而这正是我们的调用者所关心的条件。
 * （此外，内部页面从不标记为半死，因此甚至不可能_直接_评估一个内部页面是否是某个待删除子树的一部分。）
 */
static bool fc__bt_rightsib_halfdeadflag(Relation fc_rel, BlockNumber fc_leafrightsib)
{
	Buffer		fc_buf;
	Page		fc_page;
	BTPageOpaque fc_opaque;
	bool		fc_result;

	Assert(fc_leafrightsib != P_NONE);

	fc_buf = _bt_getbuf(fc_rel, fc_leafrightsib, BT_READ);
	fc_page = BufferGetPage(fc_buf);
	fc_opaque = BTPageGetOpaque(fc_page);

	Assert(P_ISLEAF(fc_opaque) && !P_ISDELETED(fc_opaque));
	fc_result = P_ISHALFDEAD(fc_opaque);
	_bt_relbuf(fc_rel, fc_buf);

	return fc_result;
}

/*
 * _bt_pagedel() -- 从b树中删除一个叶子页面，如果这样做是合法的。
 *
 * 此操作将叶子页面从b树结构中解除链接，移除指向它的所有
 * 指针——但不触及它的左右链接。该页面不能立即被物理回收，
 * 因为其他进程可能正在尝试跟随指向该页面的链接；必须允许它们
 * 使用其右链接进行恢复。请参见nbtree/README。
 *
 * 进入时，目标缓冲区必须被固定和锁定（读取或写入锁都可以）。
 * 页面必须是一个空的叶子页面，可能已经是半死状态（半死页面
 * 应仅在早期的VACUUM操作被中断时传递给我们）。特别注意，
 * 调用者不应在此传递包含已存在删除页面的缓冲区。在返回之前，
 * 调用者的缓冲区上的锁和固定将被释放。
 *
 * 为调用者维护批量删除统计信息，这些信息来自vstate。我们
 * 需要与调用者密切合作，以便整个VACUUM操作能够可靠地避免
 * 任何对子叶缓冲区页面的重复计数。如果这些页面恰好来自于
 * 当前位置的块号大于当前的scanblkno，那么预计调用者稍后会
 * 直接计数它们。理解调用者的要求对我们来说更简单，
 * 而调用者理解一个删除页面在事后何时或如何被删除则更困难。
 *
 * 注意：这会导致内存泄漏。与其尝试仔细清理所有内容，
 * 不如在可以频繁重置的临时上下文中运行它。
 */
void _bt_pagedel(Relation fc_rel, Buffer fc_leafbuf, BTVacState *fc_vstate)
{
	BlockNumber fc_rightsib;
	bool		fc_rightsib_empty;
	Page		fc_page;
	BTPageOpaque fc_opaque;

	/*
	 * 保存调用者的原始叶子缓冲区块号。只有小于等于scanblkno的已删除块
	 * 被添加到批量删除统计的pages_deleted计数中。
	 */
	BlockNumber fc_scanblkno = BufferGetBlockNumber(fc_leafbuf);

	/*
	 * "stack"是一个搜索堆栈，(大致)指向目标页面。
	 * 它最初为NULL，但在迭代时，我们保持它以避免
	 * 重复的搜索工作。
	 *
	 * 此外，当"stack"不为NULL时，我们已经检查过
	 * 当前页面不是一个不完整分裂的右半部分，即左兄弟
	 * 没有设置其INCOMPLETE_SPLIT标志，包括
	 * 当前目标页面位于调用者初始页面的右侧
	 * （scanblkno页面）时。
	 */
	BTStack		fc_stack = NULL;

	for (;;)
	{
		fc_page = BufferGetPage(fc_leafbuf);
		fc_opaque = BTPageGetOpaque(fc_page);

		/*
		 * 内部页面从不直接删除，仅作为删除
		 * 整个子树到叶子级别的一部分。
		 *
		 * 此处也检查已删除页面。调用者从不向我们传递完全
		 * 删除的页面。只有VACUUM可以删除页面，因此不能
		 * 发生并发删除。假设我们通过跟随兄弟链接达到了
		 * 这里遇到的任何已删除页面，并且索引是损坏的。
		 */
		Assert(!P_ISDELETED(fc_opaque));
		if (!P_ISLEAF(fc_opaque) || P_ISDELETED(fc_opaque))
		{
			/*
			 * 9.4版本之前的页面删除仅将内部页面标记为半死状态，
			 * 但现在我们只在叶子页面上使用这个标记。旧的算法
			 * 本来不应该在树中留下半死的页面，这只是一个临时状态，
			 * 但在错误场景中，仍然有可能发生。我们不知道如何处理
			 * 它们。在搜索方面，它们是无害的，但对已删除的键空间
			 * 进行插入可能会在上层添加无序的下链接。记录一个通知，
			 * 希望管理员会注意到并重新建立索引。
			 */
			if (P_ISHALFDEAD(fc_opaque))
				ereport(LOG,
						(errcode(ERRCODE_INDEX_CORRUPTED),
						 errmsg("index \"%s\" contains a half-dead internal page",
								RelationGetRelationName(fc_rel)),
						 errhint("This can be caused by an interrupted VACUUM in version 9.3 or older, before upgrade. Please REINDEX it.")));

			if (P_ISDELETED(fc_opaque))
				ereport(LOG,
						(errcode(ERRCODE_INDEX_CORRUPTED),
						 errmsg_internal("found deleted block %u while following right link from block %u in index \"%s\"",
										 BufferGetBlockNumber(fc_leafbuf),
										 fc_scanblkno,
										 RelationGetRelationName(fc_rel))));

			_bt_relbuf(fc_rel, fc_leafbuf);
			return;
		}

		/*
		 * 我们永远无法删除最右侧的页面或根页面。在此期间，
		 * 检查页面是否为空，因为叶子缓冲区页面可能在一瞬间
		 * 是空的，但随后已经进行了某些插入。
		 *
		 * 为了保持算法简单，我们也永远不会删除一个不完整
		 * 分裂的页面（它们应该足够少，以至于这不会对磁盘
		 * 使用产生任何有意义的影响）：
		 *
		 * 页面上的 INCOMPLETE_SPLIT 标志告诉我们页面是否是
		 * 不完整分裂的左半部分，但确保它不是右半部分则更为复杂。
		 * 为此，我们必须检查左侧兄弟节点没有设置 INCOMPLETE_SPLIT
		 * 标志，使用 _bt_leftsib_splitflag()。在第一次迭代中，
		 * 我们暂时释放对 scanblkno/leafbuf 的锁，检查左侧兄弟
		 * 节点，并构建一个搜索栈以查询 scanblkno。在后续迭代中，
		 * 我们知道我们是从一个通过这些测试的页面向右移动的，所以
		 * 这是可以的。
		 */
		if (P_RIGHTMOST(fc_opaque) || P_ISROOT(fc_opaque) ||
			P_FIRSTDATAKEY(fc_opaque) <= PageGetMaxOffsetNumber(fc_page) ||
			P_INCOMPLETE_SPLIT(fc_opaque))
		{
			/* 不应该失败于删除半死页面 */
			Assert(!P_ISHALFDEAD(fc_opaque));

			_bt_relbuf(fc_rel, fc_leafbuf);
			return;
		}

		/*
		 * 首先，移除指向页面的下链接（如果我们要删除一个更高
		 * 的子树，则指向页面的父页面），并将叶子缓冲区页面
		 * 标记为半死状态
		 */
		if (!P_ISHALFDEAD(fc_opaque))
		{
			/*
			 * 我们需要一个指向页面父页面的近似指针。我们使用
			 * 标准搜索机制的一种变体来搜索页面的高键；这将为
			 * 我们提供一个链接，指向当前的父页面或其左侧的某处
			 * （如果有多个相等的高键，这在非堆键空间索引中是
			 * 可能的）。
			 *
			 * 还要检查这是否是一个不完整分裂的右半部分
			 * （参见上面的注释）。
			 */
			if (!fc_stack)
			{
				BTScanInsert fc_itup_key;
				ItemId		fc_itemid;
				IndexTuple	fc_targetkey;
				BlockNumber fc_leftsib,
							fc_leafblkno;
				Buffer		fc_sleafbuf;

				fc_itemid = PageGetItemId(fc_page, P_HIKEY);
				fc_targetkey = CopyIndexTuple((IndexTuple) PageGetItem(fc_page, fc_itemid));

				fc_leftsib = fc_opaque->btpo_prev;
				fc_leafblkno = BufferGetBlockNumber(fc_leafbuf);

				/*
				 * 为了避免死锁，我们最好在进一步操作之前释放
				 * 叶页面锁。
				 */
				_bt_unlockbuf(fc_rel, fc_leafbuf);

				/*
				 * 在继续之前，检查叶子缓冲区的左侧兄弟（如果有）
				 * 是否未被标记为 INCOMPLETE_SPLIT。
				 */
				Assert(fc_leafblkno == fc_scanblkno);
				if (fc__bt_leftsib_splitflag(fc_rel, fc_leftsib, fc_leafblkno))
				{
					ReleaseBuffer(fc_leafbuf);
					return;
				}

				/* 我们需要一个插入扫描键来进行搜索，因此构建一个 */
				fc_itup_key = _bt_mkscankey(fc_rel, fc_targetkey);
				/* 找到具有匹配的枢轴/高键的最左叶页 */
				fc_itup_key->pivotsearch = true;
				fc_stack = _bt_search(fc_rel, fc_itup_key, &fc_sleafbuf, BT_READ, NULL);
				/* 不需要对叶子缓冲区进行第二个锁定或固定 */
				_bt_relbuf(fc_rel, fc_sleafbuf);

				/*
				 * 重新锁定叶子页面，并重新开始使用我们的栈
				 * 在_bt_mark_page_halfdead中。我们必须这样做
				 * 因为叶子缓冲区可能无法再被
				 * 删除。我们需要重新检查。
				 *
				 * 注意：我们不能简单地保持对sleafbuf锁定，
				 * 因为sleafbuf和leafbuf可能并不是同一个
				 * 页面。这发生在leafbuf在我们
				 * 最初的锁定被释放后分裂，但在_bt_search完成
				 * 它的下降之前。我们依赖于一个假设，即在这种情况下
				 * 我们会发现leafbuf不再安全可删除。
				 * （页面删除可以处理栈位于leafbuf左侧的情况，
				 * 但不能处理位于leafbuf右侧的情况。）
				 */
				_bt_lockbuf(fc_rel, fc_leafbuf, BT_WRITE);
				continue;
			}

			/*
			 * 查看删除叶子页面是否安全，并确定有多少个父页面/internal 
			 * 页面将在叶子级别以上被删除。如果安全，则_bt_mark_page_halfdead也将
			 * 执行删除的第一阶段，包括将leafbuf页面标记为半死状态。
			 */
			Assert(P_ISLEAF(fc_opaque) && !P_IGNORE(fc_opaque));
			if (!fc__bt_mark_page_halfdead(fc_rel, fc_leafbuf, fc_stack))
			{
				_bt_relbuf(fc_rel, fc_leafbuf);
				return;
			}
		}

		/*
		 * 然后将其从其兄弟页面中解除链接。每次调用
		 * _bt_unlink_halfdead_page都会将最顶部的页面从子树中解除链接，
		 * 使其变得更浅。迭代直到leafbuf页面被删除。
		 */
		fc_rightsib_empty = false;
		Assert(P_ISLEAF(fc_opaque) && P_ISHALFDEAD(fc_opaque));
		while (P_ISHALFDEAD(fc_opaque))
		{
			/* 检查_bt_unlink_halfdead_page中的中断 */
			if (!fc__bt_unlink_halfdead_page(fc_rel, fc_leafbuf, fc_scanblkno,
										  &fc_rightsib_empty, fc_vstate))
			{
				/*
				 * _bt_unlink_halfdead_page应该永远不会失败，因为我们
				 * 确定删除在_bt_mark_page_halfdead中通常是安全的——索引必须是损坏的。
				 *
				 * 注意，_bt_unlink_halfdead_page已经为我们释放了
				 * leafbuf上的锁和针锁。
				 */
				Assert(false);
				return;
			}
		}

		Assert(P_ISLEAF(fc_opaque) && P_ISDELETED(fc_opaque));

		fc_rightsib = fc_opaque->btpo_next;

		_bt_relbuf(fc_rel, fc_leafbuf);

		/*
		 * 在这里检查，因为调用循环将保持锁定，
		 * 防止处理中断。
		 */
		CHECK_FOR_INTERRUPTS();

		/*
		 * 页面现在已被删除。如果其右兄弟页面完全
		 * 为空，那么我们之前未删除它的原因可能是
		 * 它是父页面的右most子节点。现在我们
		 * 移除了此页面的下行链接，右兄弟现在可能是
		 * 父页面的唯一子节点，并且可以被移除。无论如何，它将由下一个清理操作拾取，
		 * 但现在也可以尝试删除它，因此循环回去处理右兄弟。
		 *
		 * 注意：这依赖于假设_bt_getstackbuf()将能够
		 * 重新利用我们原始的下降栈与不同的子
		 * 块（前提是子块在由_bt_search()到达的
		 * 原始叶子页面的右侧）。它甚至会在每次循环时更新
		 * 下降栈，避免重复工作。
		 */
		if (!fc_rightsib_empty)
			break;

		fc_leafbuf = _bt_getbuf(fc_rel, fc_rightsib, BT_WRITE);
	}
}

/*
 * 页面的删除第一阶段。
 *
 * 确定要删除的子树的高度，将leafbuf置于其最低级别，移除指向
 * 子树的下链接，并标记leafbuf为半死状态。最终要删除的子树
 * 通常就是leafbuf本身，但也可能包括额外的内部页面（在根
 * 节点以下的每一层最多一个）。
 *
 * 如果leafbuf不安全被删除（通常是因为leafbuf是其父节点的
 * 最右边的子节点，并且父节点有多个下链接），则返回'false'。
 * 当页面删除的第一阶段成功完成时返回'true'。
 */
static bool fc__bt_mark_page_halfdead(Relation fc_rel, Buffer fc_leafbuf, BTStack fc_stack)
{
	BlockNumber fc_leafblkno;
	BlockNumber fc_leafrightsib;
	BlockNumber fc_topparent;
	BlockNumber fc_topparentrightsib;
	ItemId		fc_itemid;
	Page		fc_page;
	BTPageOpaque fc_opaque;
	Buffer		fc_subtreeparent;
	OffsetNumber fc_poffset;
	OffsetNumber fc_nextoffset;
	IndexTuple	fc_itup;
	IndexTupleData fc_trunctuple;

	fc_page = BufferGetPage(fc_leafbuf);
	fc_opaque = BTPageGetOpaque(fc_page);

	Assert(!P_RIGHTMOST(fc_opaque) && !P_ISROOT(fc_opaque) &&
		   P_ISLEAF(fc_opaque) && !P_IGNORE(fc_opaque) &&
		   P_FIRSTDATAKEY(fc_opaque) > PageGetMaxOffsetNumber(fc_page));

	/*
	 * 保存关于叶子页面的信息。
	 */
	fc_leafblkno = BufferGetBlockNumber(fc_leafbuf);
	fc_leafrightsib = fc_opaque->btpo_next;

	/*
	 * 在尝试锁定父页面之前，检查右兄弟是否不是半死状态。
	 * 半死的右兄弟在父节点中将没有下链接，这在我们删除
	 * 下链接时会极为混淆。它会失败下面的“目标页面的右兄弟
	 * 也是父页面中的下一个子节点”的交叉检查。
	 */
	if (fc__bt_rightsib_halfdeadflag(fc_rel, fc_leafrightsib))
	{
		elog(DEBUG1, "could not delete page %u because its right sibling %u is half-dead",
			 fc_leafblkno, fc_leafrightsib);
		return false;
	}

	/*
	 * 我们不能删除作为其直接父节点的最右子节点的页面，
	 * 除非它是唯一的子节点——在这种情况下，父节点也
	 * 必须被删除，并且相同的条件也适用于它。我们必须在
	 * 尝试删除之前检查这个条件，一直到根节点的父节点
	 * (“子树父节点”)。_bt_lock_subtree_parent()为我们锁定
	 * 子树父节点。我们从以下子树父节点页面中移除指向
	 * “顶级父节点”页面（子树根页面）的下链接。
	 *
	 * 现在初始化topparent为leafbuf页面。最终要删除的
	 * 子树通常是一个退化的单页面子树，仅由leafbuf页面
	 * 组成。当这种情况发生时，leafbuf页面就是最终
	 * 子树根页面/顶级父页面。
	 */
	fc_topparent = fc_leafblkno;
	fc_topparentrightsib = fc_leafrightsib;
	if (!fc__bt_lock_subtree_parent(fc_rel, fc_leafblkno, fc_stack,
								 &fc_subtreeparent, &fc_poffset,
								 &fc_topparent, &fc_topparentrightsib))
		return false;

	fc_page = BufferGetPage(fc_subtreeparent);
	fc_opaque = BTPageGetOpaque(fc_page);

#ifdef USE_ASSERT_CHECKING

	/*
	 * 这只是一个断言，因为_bt_lock_subtree_parent应该
	 * 保证元组具有预期的内容
	 */
	fc_itemid = PageGetItemId(fc_page, fc_poffset);
	fc_itup = (IndexTuple) PageGetItem(fc_page, fc_itemid);
	Assert(BTreeTupleGetDownLink(fc_itup) == fc_topparent);
#endif

	fc_nextoffset = OffsetNumberNext(fc_poffset);
	fc_itemid = PageGetItemId(fc_page, fc_nextoffset);
	fc_itup = (IndexTuple) PageGetItem(fc_page, fc_itemid);

	/*
	 * 检查在子树父页面中我们即将删除/覆盖的父页面索引
	 * 项目是否包含我们期望的内容。如果因某种原因
	 * 索引变得损坏，这可能会失败。当这种情况发生时，
	 * 我们将撤销leafbuf子树的删除。（这就像
	 * _bt_lock_subtree_parent()无法“重新找到”leafbuf的
	 * 下链接的情况。）
	 */
	if (BTreeTupleGetDownLink(fc_itup) != fc_topparentrightsib)
	{
		ereport(LOG,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg_internal("right sibling %u of block %u is not next child %u of block %u in index \"%s\"",
								 fc_topparentrightsib, fc_topparent,
								 BTreeTupleGetDownLink(fc_itup),
								 BufferGetBlockNumber(fc_subtreeparent),
								 RelationGetRelationName(fc_rel))));

		_bt_relbuf(fc_rel, fc_subtreeparent);
		Assert(false);
		return false;
	}

	/*
	 * 任何原本应放在叶子块上的插入现在将转到其
	 * 右兄弟。换句话说，关键空间向右移动。
	 */
	PredicateLockPageCombine(fc_rel, fc_leafblkno, fc_leafrightsib);

	/* 直到更改被记录，不要ereport(ERROR) */
	START_CRIT_SECTION();

	/*
	 * 更新子树的父节点。我们想要删除指向子树的顶级父页面/根页面的下行链接，以及*以下*键。最简单的方法是将右侧兄弟的下行链接复制到指向顶级父页面的下行链接上，然后删除右侧兄弟的原始枢轴元组。
	 *
	 * Lanin 和 Shasha 在删除页面时使键空间向左移动，而这里的键空间向右移动。这就是为什么我们不能简单地删除与顶级父页面的下行链接的枢轴元组。详见 nbtree/README。
	 */
	fc_page = BufferGetPage(fc_subtreeparent);
	fc_opaque = BTPageGetOpaque(fc_page);

	fc_itemid = PageGetItemId(fc_page, fc_poffset);
	fc_itup = (IndexTuple) PageGetItem(fc_page, fc_itemid);
	BTreeTupleSetDownLink(fc_itup, fc_topparentrightsib);

	fc_nextoffset = OffsetNumberNext(fc_poffset);
	PageIndexTupleDelete(fc_page, fc_nextoffset);

	/*
	 * 将叶子页面标记为半死状态，并用指向顶级父页面的链接戳记它。当叶子页面也是顶级父页面时，链接设置为 InvalidBlockNumber。
	 */
	fc_page = BufferGetPage(fc_leafbuf);
	fc_opaque = BTPageGetOpaque(fc_page);
	fc_opaque->btpo_flags |= BTP_HALF_DEAD;

	Assert(PageGetMaxOffsetNumber(fc_page) == P_HIKEY);
	MemSet(&fc_trunctuple, 0, sizeof(IndexTupleData));
	fc_trunctuple.t_info = sizeof(IndexTupleData);
	if (fc_topparent != fc_leafblkno)
		BTreeTupleSetTopParent(&fc_trunctuple, fc_topparent);
	else
		BTreeTupleSetTopParent(&fc_trunctuple, InvalidBlockNumber);

	if (!PageIndexTupleOverwrite(fc_page, P_HIKEY, (Item) &fc_trunctuple,
								 IndexTupleSize(&fc_trunctuple)))
		elog(ERROR, "could not overwrite high key in half-dead page");

	/* 必须在 XLogInsert 之前标记缓存脏 */
	MarkBufferDirty(fc_subtreeparent);
	MarkBufferDirty(fc_leafbuf);

	/* XLOG 相关内容 */
	if (RelationNeedsWAL(fc_rel))
	{
		xl_btree_mark_page_halfdead fc_xlrec;
		XLogRecPtr	fc_recptr;

		fc_xlrec.poffset = fc_poffset;
		fc_xlrec.leafblk = fc_leafblkno;
		if (fc_topparent != fc_leafblkno)
			fc_xlrec.topparent = fc_topparent;
		else
			fc_xlrec.topparent = InvalidBlockNumber;

		XLogBeginInsert();
		XLogRegisterBuffer(0, fc_leafbuf, REGBUF_WILL_INIT);
		XLogRegisterBuffer(1, fc_subtreeparent, REGBUF_STANDARD);

		fc_page = BufferGetPage(fc_leafbuf);
		fc_opaque = BTPageGetOpaque(fc_page);
		fc_xlrec.leftblk = fc_opaque->btpo_prev;
		fc_xlrec.rightblk = fc_opaque->btpo_next;

		XLogRegisterData((char *) &fc_xlrec, SizeOfBtreeMarkPageHalfDead);

		fc_recptr = XLogInsert(RM_BTREE_ID, XLOG_BTREE_MARK_PAGE_HALFDEAD);

		fc_page = BufferGetPage(fc_subtreeparent);
		PageSetLSN(fc_page, fc_recptr);
		fc_page = BufferGetPage(fc_leafbuf);
		PageSetLSN(fc_page, fc_recptr);
	}

	END_CRIT_SECTION();

	_bt_relbuf(fc_rel, fc_subtreeparent);
	return true;
}

/*
 * 页面删除的第二阶段。
 *
 * 从其兄弟节点中解除单个页面（正在进行删除的子树中的页面）的链接。还将该页面标记为已删除。
 *
 * 要摆脱整个子树，包括叶子页面本身，请在此调用，直到叶子页面被删除。在此处的第一次调用中删除了在删除第一阶段建立的原始“顶级父节点”，而在此处的最后一次调用中删除了叶子页面。请注意，叶子页面本身通常就是最初的顶级父页面。
 *
 * 如果页面无法解除链接，则返回 'false'（不应该发生）。如果当前目标页面的右侧兄弟为空，则将 *rightsib_empty 设置为 true，允许调用者在经过时删除目标的右侧兄弟页面。请注意，*rightsib_empty 仅在目标页面为 leafbuf 时由调用者实际使用，遵循这里对 leafbuf/the 子树的最后调用。（我们总是为调用者设置 *rightsib_empty，只是为了保持一致。）
 *
 * 在入口时必须对 leafbuf 持有 pin 和锁（读取或写入无关紧要）。在成功退出时，我们将持有 pin 和写锁。在失败退出时，我们将在返回之前释放 pin 和锁（我们这样定义是为了避免重新获取已释放的锁）。
 */
static bool fc__bt_unlink_halfdead_page(Relation fc_rel, Buffer fc_leafbuf, BlockNumber fc_scanblkno,
						 bool *fc_rightsib_empty, BTVacState *fc_vstate)
{
	BlockNumber fc_leafblkno = BufferGetBlockNumber(fc_leafbuf);
	IndexBulkDeleteResult *fc_stats = fc_vstate->stats;
	BlockNumber fc_leafleftsib;
	BlockNumber fc_leafrightsib;
	BlockNumber fc_target;
	BlockNumber fc_leftsib;
	BlockNumber fc_rightsib;
	Buffer		fc_lbuf = InvalidBuffer;
	Buffer		fc_buf;
	Buffer		fc_rbuf;
	Buffer		fc_metabuf = InvalidBuffer;
	Page		fc_metapg = NULL;
	BTMetaPageData *fc_metad = NULL;
	ItemId		fc_itemid;
	Page		fc_page;
	BTPageOpaque fc_opaque;
	FullTransactionId fc_safexid;
	bool		fc_rightsib_is_rightmost;
	uint32		fc_targetlevel;
	IndexTuple	fc_leafhikey;
	BlockNumber fc_leaftopparent;

	fc_page = BufferGetPage(fc_leafbuf);
	fc_opaque = BTPageGetOpaque(fc_page);

	Assert(P_ISLEAF(fc_opaque) && !P_ISDELETED(fc_opaque) && P_ISHALFDEAD(fc_opaque));

	/*
	 * 记住有关叶子页面的一些信息。
	 */
	fc_itemid = PageGetItemId(fc_page, P_HIKEY);
	fc_leafhikey = (IndexTuple) PageGetItem(fc_page, fc_itemid);
	fc_target = BTreeTupleGetTopParent(fc_leafhikey);
	fc_leafleftsib = fc_opaque->btpo_prev;
	fc_leafrightsib = fc_opaque->btpo_next;

	_bt_unlockbuf(fc_rel, fc_leafbuf);

	/*
	 * 在这里检查，因为调用循环将会持有锁，防止中断被处理。
	 */
	CHECK_FOR_INTERRUPTS();

	/* 解除子树当前顶级父节点的链接 */
	if (!BlockNumberIsValid(fc_target))
	{
		/* 目标是叶子页面（或者如果你愿意，叶子页面是顶级父页面） */
		fc_target = fc_leafblkno;

		fc_buf = fc_leafbuf;
		fc_leftsib = fc_leafleftsib;
		fc_targetlevel = 0;
	}
	else
	{
		/* 目标是从叶子顶部父链接获取的内部页面 */
		Assert(fc_target != fc_leafblkno);

		/* 获取目标的左侧兄弟的块号 */
		fc_buf = _bt_getbuf(fc_rel, fc_target, BT_READ);
		fc_page = BufferGetPage(fc_buf);
		fc_opaque = BTPageGetOpaque(fc_page);
		fc_leftsib = fc_opaque->btpo_prev;
		fc_targetlevel = fc_opaque->btpo_level;
		Assert(fc_targetlevel > 0);

		/*
		 * 为了避免死锁，我们最好在进一步操作之前放弃目标页面的锁。
		 */
		_bt_unlockbuf(fc_rel, fc_buf);
	}

	/*
	 * 我们必须按照标准顺序锁定需要修改的页面：
	 * 向右移动，然后向上。 否则我们会与其他写入者发生死锁。
	 *
	 * 所以，首先锁定叶子页面，如果它不是目标。 然后查找并
	 * 写锁定目标页面的当前左兄弟。 片刻之前是当前的兄弟可能已经分裂，所以我们可能需要向右移动。
	 */
	if (fc_target != fc_leafblkno)
		_bt_lockbuf(fc_rel, fc_leafbuf, BT_WRITE);
	if (fc_leftsib != P_NONE)
	{
		fc_lbuf = _bt_getbuf(fc_rel, fc_leftsib, BT_WRITE);
		fc_page = BufferGetPage(fc_lbuf);
		fc_opaque = BTPageGetOpaque(fc_page);
		while (P_ISDELETED(fc_opaque) || fc_opaque->btpo_next != fc_target)
		{
			bool		fc_leftsibvalid = true;

			/*
			 * 在我们跟随从刚刚是左兄弟的页面的链接之前，验证它的右链接。 这
			 * 减少了在索引损坏的情况下循环无法取得任何进展的机会。
			 *
			 * 注意：我们依赖于假设在同一时间只能有一个
			 * vacuum 进程在运行（针对同一索引）。
			 */
			if (P_RIGHTMOST(fc_opaque) || P_ISDELETED(fc_opaque) ||
				fc_leftsib == fc_opaque->btpo_next)
				fc_leftsibvalid = false;

			fc_leftsib = fc_opaque->btpo_next;
			_bt_relbuf(fc_rel, fc_lbuf);

			if (!fc_leftsibvalid)
			{
				/*
				 * 这是已知在实际中会失败的；兄弟链接损坏
				 * 相对常见。 继续执行 vacuum 而不仅仅是
				 * 抛出错误。
				 */
				ereport(LOG,
						(errcode(ERRCODE_INDEX_CORRUPTED),
						 errmsg_internal("valid left sibling for deletion target could not be located: "
										 "left sibling %u of target %u with leafblkno %u and scanblkno %u on level %u of index \"%s\"",
										 fc_leftsib, fc_target, fc_leafblkno, fc_scanblkno,
										 fc_targetlevel, RelationGetRelationName(fc_rel))));

				/* 出现故障退出时必须释放所有的引脚和锁 */
				ReleaseBuffer(fc_buf);
				if (fc_target != fc_leafblkno)
					_bt_relbuf(fc_rel, fc_leafbuf);

				return false;
			}

			CHECK_FOR_INTERRUPTS();

			/* 向右移动一页 */
			fc_lbuf = _bt_getbuf(fc_rel, fc_leftsib, BT_WRITE);
			fc_page = BufferGetPage(fc_lbuf);
			fc_opaque = BTPageGetOpaque(fc_page);
		}
	}
	else
		fc_lbuf = InvalidBuffer;

	/* 接下来写锁定目标页面本身 */
	_bt_lockbuf(fc_rel, fc_buf, BT_WRITE);
	fc_page = BufferGetPage(fc_buf);
	fc_opaque = BTPageGetOpaque(fc_page);

	/*
	 * 检查页面是否仍然为空等，否则放弃删除。 这只是出于
	 * 怀疑；一个半死的页面无法复活，因为一次只能有一个 vacuum 进程在运行。
	 */
	if (P_RIGHTMOST(fc_opaque) || P_ISROOT(fc_opaque) || P_ISDELETED(fc_opaque))
		elog(ERROR, "target page changed status unexpectedly in block %u of index \"%s\"",
			 fc_target, RelationGetRelationName(fc_rel));

	if (fc_opaque->btpo_prev != fc_leftsib)
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg_internal("target page left link unexpectedly changed from %u to %u in block %u of index \"%s\"",
								 fc_leftsib, fc_opaque->btpo_prev, fc_target,
								 RelationGetRelationName(fc_rel))));

	if (fc_target == fc_leafblkno)
	{
		if (P_FIRSTDATAKEY(fc_opaque) <= PageGetMaxOffsetNumber(fc_page) ||
			!P_ISLEAF(fc_opaque) || !P_ISHALFDEAD(fc_opaque))
			elog(ERROR, "target leaf page changed status unexpectedly in block %u of index \"%s\"",
				 fc_target, RelationGetRelationName(fc_rel));

		/* 叶子页面也是目标页面：不要设置 leaftopparent */
		fc_leaftopparent = InvalidBlockNumber;
	}
	else
	{
		IndexTuple	fc_finaldataitem;

		if (P_FIRSTDATAKEY(fc_opaque) != PageGetMaxOffsetNumber(fc_page) ||
			P_ISLEAF(fc_opaque))
			elog(ERROR, "target internal page on level %u changed status unexpectedly in block %u of index \"%s\"",
				 fc_targetlevel, fc_target, RelationGetRelationName(fc_rel));

		/* 目标是内部的：为下一个调用设置 leaftopparent 在这里…  */
		fc_itemid = PageGetItemId(fc_page, P_FIRSTDATAKEY(fc_opaque));
		fc_finaldataitem = (IndexTuple) PageGetItem(fc_page, fc_itemid);
		fc_leaftopparent = BTreeTupleGetDownLink(fc_finaldataitem);
		/* …除非这将是一个冗余的自指针 */
		if (fc_leaftopparent == fc_leafblkno)
			fc_leaftopparent = InvalidBlockNumber;
	}

	/* 对于0级（叶子页面）或1级目标，没有父叶子页面 */
	Assert(!BlockNumberIsValid(fc_leaftopparent) || fc_targetlevel > 1);

	/*
	 * 然后写锁定（当前）右兄弟。
	 */
	fc_rightsib = fc_opaque->btpo_next;
	fc_rbuf = _bt_getbuf(fc_rel, fc_rightsib, BT_WRITE);
	fc_page = BufferGetPage(fc_rbuf);
	fc_opaque = BTPageGetOpaque(fc_page);

	/*
	 * 验证目标的右兄弟页面。 其左链接必须指回
	 * 目标页面。
	 */
	if (fc_opaque->btpo_prev != fc_target)
	{
		/*
		 * 这是已知在实际中会失败的；兄弟链接损坏是
		 * 相对常见。 继续执行 vacuum 而不仅仅是
		 * 抛出错误（与刚才用于左兄弟的右链接
		 * 验证检查相同的方法）。
		 */
		ereport(LOG,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg_internal("right sibling's left-link doesn't match: "
								 "right sibling %u of target %u with leafblkno %u "
								 "and scanblkno %u spuriously links to non-target %u "
								 "on level %u of index \"%s\"",
								 fc_rightsib, fc_target, fc_leafblkno,
								 fc_scanblkno, fc_opaque->btpo_prev,
								 fc_targetlevel, RelationGetRelationName(fc_rel))));

		/* 出现故障退出时必须释放所有的引脚和锁 */
		if (BufferIsValid(fc_lbuf))
			_bt_relbuf(fc_rel, fc_lbuf);
		_bt_relbuf(fc_rel, fc_rbuf);
		_bt_relbuf(fc_rel, fc_buf);
		if (fc_target != fc_leafblkno)
			_bt_relbuf(fc_rel, fc_leafbuf);

		return false;
	}

	fc_rightsib_is_rightmost = P_RIGHTMOST(fc_opaque);
	*fc_rightsib_empty = (P_FIRSTDATAKEY(fc_opaque) > PageGetMaxOffsetNumber(fc_page));

	/*
	 * 如果我们正在删除目标级别上的倒数第二页，那么
	 * rightsib 是成为新快速根的候选者。 （理论上，可能可以将快速根进一步向下推，但这样做的机会很小，锁定考虑事项也令人望而生畏。）
	 *
	 * 我们可以安全地在这里获取 metapage 的锁 --- 请参见
	 * _bt_newroot() 的注释。
	 */
	if (fc_leftsib == P_NONE && fc_rightsib_is_rightmost)
	{
		fc_page = BufferGetPage(fc_rbuf);
		fc_opaque = BTPageGetOpaque(fc_page);
		if (P_RIGHTMOST(fc_opaque))
		{
			/* rightsib 将是该级别上唯一剩下的一个 */
			fc_metabuf = _bt_getbuf(fc_rel, BTREE_METAPAGE, BT_WRITE);
			fc_metapg = BufferGetPage(fc_metabuf);
			fc_metad = BTPageGetMeta(fc_metapg);

			/*
			 * 这里的预期情况是 btm_fastlevel == targetlevel + 1；如果
			 * fastlevel <= targetlevel，那么就出了问题，我们选择覆盖它以修复它。
			 */
			if (fc_metad->btm_fastlevel > fc_targetlevel + 1)
			{
				/* 不需要更新 */
				_bt_relbuf(fc_rel, fc_metabuf);
				fc_metabuf = InvalidBuffer;
			}
		}
	}

	/*
	 * 在这里我们开始进行删除。
	 */

	/* 直到更改被记录，不要ereport(ERROR) */
	START_CRIT_SECTION();

	/*
	 * 更新兄弟节点的侧链接。 请注意，目标页面的侧链接将继续指向兄弟节点。 此处的断言只是重新检查我们之前验证过的内容。
	 */
	if (BufferIsValid(fc_lbuf))
	{
		fc_page = BufferGetPage(fc_lbuf);
		fc_opaque = BTPageGetOpaque(fc_page);
		Assert(fc_opaque->btpo_next == fc_target);
		fc_opaque->btpo_next = fc_rightsib;
	}
	fc_page = BufferGetPage(fc_rbuf);
	fc_opaque = BTPageGetOpaque(fc_page);
	Assert(fc_opaque->btpo_prev == fc_target);
	fc_opaque->btpo_prev = fc_leftsib;

	/*
	 * 如果我们删除了目标叶子页面的父节点，而不是叶子本身，更新叶子以指向子树中下一个剩余的子节点。
	 *
	 * 注意：我们依赖于自 leafhikey 初始化以来已经持有叶子页面的缓冲区锁定。 这是安全的，尽管只有因为在那时页面已经半死了。 在没有持有锁的期间，叶子页面不能被任何其他后端修改。
	 */
	if (fc_target != fc_leafblkno)
		BTreeTupleSetTopParent(fc_leafhikey, fc_leaftopparent);

	/*
	 * 标记页面本身为已删除。 当所有当前的事务消失时，它可以被回收。 存储 GetTopTransactionId() 是可行的，但我们在 VACUUM 中，并且否则不会有 XID。 已经更新了指向目标的链接，ReadNextFullTransactionId() 足以作为一个上限。 任何保留了现在过时链接的扫描在其 PGPROC 中宣传一个 xmin 小于或等于我们在这里读取的值。 在该扫描持续期间，它会继续这样做，阻碍 xmin 地平线。
	 */
	fc_page = BufferGetPage(fc_buf);
	fc_opaque = BTPageGetOpaque(fc_page);
	Assert(P_ISHALFDEAD(fc_opaque) || !P_ISLEAF(fc_opaque));

	/*
	 * 存储用于确定删除页面何时不再需要作为墓碑的上限 XID
	 */
	fc_safexid = ReadNextFullTransactionId();
	BTPageSetDeleted(fc_page, fc_safexid);
	fc_opaque->btpo_cycleid = 0;

	/* 如果需要，更新元页面 */
	if (BufferIsValid(fc_metabuf))
	{
		/* 根据需要升级元数据页面 */
		if (fc_metad->btm_version < BTREE_NOVAC_VERSION)
			_bt_upgrademetapage(fc_metapg);
		fc_metad->btm_fastroot = fc_rightsib;
		fc_metad->btm_fastlevel = fc_targetlevel;
		MarkBufferDirty(fc_metabuf);
	}

	/* 必须在 XLogInsert 之前标记缓存脏 */
	MarkBufferDirty(fc_rbuf);
	MarkBufferDirty(fc_buf);
	if (BufferIsValid(fc_lbuf))
		MarkBufferDirty(fc_lbuf);
	if (fc_target != fc_leafblkno)
		MarkBufferDirty(fc_leafbuf);

	/* XLOG 相关内容 */
	if (RelationNeedsWAL(fc_rel))
	{
		xl_btree_unlink_page fc_xlrec;
		xl_btree_metadata fc_xlmeta;
		uint8		fc_xlinfo;
		XLogRecPtr	fc_recptr;

		XLogBeginInsert();

		XLogRegisterBuffer(0, fc_buf, REGBUF_WILL_INIT);
		if (BufferIsValid(fc_lbuf))
			XLogRegisterBuffer(1, fc_lbuf, REGBUF_STANDARD);
		XLogRegisterBuffer(2, fc_rbuf, REGBUF_STANDARD);
		if (fc_target != fc_leafblkno)
			XLogRegisterBuffer(3, fc_leafbuf, REGBUF_WILL_INIT);

		/* 存储在目标/待解除链接块上的信息 */
		fc_xlrec.leftsib = fc_leftsib;
		fc_xlrec.rightsib = fc_rightsib;
		fc_xlrec.level = fc_targetlevel;
		fc_xlrec.safexid = fc_safexid;

		/* 重新创建叶子块所需的信息（如果不是目标） */
		fc_xlrec.leafleftsib = fc_leafleftsib;
		fc_xlrec.leafrightsib = fc_leafrightsib;
		fc_xlrec.leaftopparent = fc_leaftopparent;

		XLogRegisterData((char *) &fc_xlrec, SizeOfBtreeUnlinkPage);

		if (BufferIsValid(fc_metabuf))
		{
			XLogRegisterBuffer(4, fc_metabuf, REGBUF_WILL_INIT | REGBUF_STANDARD);

			Assert(fc_metad->btm_version >= BTREE_NOVAC_VERSION);
			fc_xlmeta.version = fc_metad->btm_version;
			fc_xlmeta.root = fc_metad->btm_root;
			fc_xlmeta.level = fc_metad->btm_level;
			fc_xlmeta.fastroot = fc_metad->btm_fastroot;
			fc_xlmeta.fastlevel = fc_metad->btm_fastlevel;
			fc_xlmeta.last_cleanup_num_delpages = fc_metad->btm_last_cleanup_num_delpages;
			fc_xlmeta.allequalimage = fc_metad->btm_allequalimage;

			XLogRegisterBufData(4, (char *) &fc_xlmeta, sizeof(xl_btree_metadata));
			fc_xlinfo = XLOG_BTREE_UNLINK_PAGE_META;
		}
		else
			fc_xlinfo = XLOG_BTREE_UNLINK_PAGE;

		fc_recptr = XLogInsert(RM_BTREE_ID, fc_xlinfo);

		if (BufferIsValid(fc_metabuf))
		{
			PageSetLSN(fc_metapg, fc_recptr);
		}
		fc_page = BufferGetPage(fc_rbuf);
		PageSetLSN(fc_page, fc_recptr);
		fc_page = BufferGetPage(fc_buf);
		PageSetLSN(fc_page, fc_recptr);
		if (BufferIsValid(fc_lbuf))
		{
			fc_page = BufferGetPage(fc_lbuf);
			PageSetLSN(fc_page, fc_recptr);
		}
		if (fc_target != fc_leafblkno)
		{
			fc_page = BufferGetPage(fc_leafbuf);
			PageSetLSN(fc_page, fc_recptr);
		}
	}

	END_CRIT_SECTION();

	/* 释放元页面 */
	if (BufferIsValid(fc_metabuf))
		_bt_relbuf(fc_rel, fc_metabuf);

	/* 释放兄弟节点 */
	if (BufferIsValid(fc_lbuf))
		_bt_relbuf(fc_rel, fc_lbuf);
	_bt_relbuf(fc_rel, fc_rbuf);

	/* 如果目标不是 leafbuf，我们现在就完成了它 - 释放它 */
	if (fc_target != fc_leafblkno)
		_bt_relbuf(fc_rel, fc_buf);

	/*
	 * 维护 pages_newly_deleted，它只是正在进行的 VACUUM 操作删除的页面数量。
	 *
	 * 以考虑到 btvacuumpage() 将如何计算尚未成为 scanblkno 的已删除页面的方式维护 pages_deleted - 仅在页面不会在后面得到这种处理时计算页面。
	 */
	fc_stats->pages_newly_deleted++;
	if (fc_target <= fc_scanblkno)
		fc_stats->pages_deleted++;

	/*
	 * 在专用的 vstate 空间中记住关于目标页面（现在是新删除的页面）的信息，以备后用。 该页面将在当前 btvacuumscan() 调用结束时被视为放置在 FSM 的候选者。
	 */
	fc__bt_pendingfsm_add(fc_vstate, fc_target, fc_safexid);

	/* 成功 - 保持对 leafbuf 的锁定（也可能是目标） */
	return true;
}

/*
 * 确定在页面删除的第一阶段中，将要删除的子树的高度。
 *
 * 调用者的子项参数是调用者想要删除的页面的块编号（这是leafbuf的块编号，除非我们被递归调用）。stack是通向它的搜索栈。请注意，我们将更新栈条目以反映当前的下链位置——这类似于页面拆分处理中的相应点。
 *
 * 如果“第一阶段”调用者无法继续删除_任何_页面，则返回false。在成功时返回true，在这种情况下，调用者可以使用这里建立的某些细节来执行删除的第一阶段。该函数是页面删除可能被认为不安全的最后一点（排除索引损坏或意外的并发页面删除）。
 *
 * 成功时，我们对要删除的子树根的父节点进行写锁（即我们将我们的锁留在*subtreeparent缓冲区中给调用者）。调用者将不得不从*subtreeparent中移除一个下链。我们还在*poffset中设置一个*subtreeparent偏移量，以指示包含相关下链的支点元组的位置。
 *
 * 要删除的子树的根称为“顶层父”。请注意，leafbuf页面通常是最后的“顶层父”页面（当发生这种情况时，您可以将leafbuf页面视为一个简化的单页子树）。调用者应初始化*topparent为目标leafbuf页面块编号（同时*topparentrightsib应设置为leafbuf的右兄弟块编号）。我们将在这里为调用者更新*topparent（和*topparentrightsib），但只有在查明调用者至少会删除一个内部页面时（即只有在调用者需要使用BTreeTupleSetTopParent()在leafbuf页面中存储到顶层父块的有效链接时）。
 */
static bool fc__bt_lock_subtree_parent(Relation fc_rel, BlockNumber fc_child, BTStack fc_stack,
						Buffer *fc_subtreeparent, OffsetNumber *fc_poffset,
						BlockNumber *fc_topparent, BlockNumber *fc_topparentrightsib)
{
	BlockNumber fc_parent,
				fc_leftsibparent;
	OffsetNumber fc_parentoffset,
				fc_maxoff;
	Buffer		fc_pbuf;
	Page		fc_page;
	BTPageOpaque fc_opaque;

	/*
	 * 定位指向“child”的下链的支点元组。对父页面本身进行写锁。
	 */
	fc_pbuf = _bt_getstackbuf(fc_rel, fc_stack, fc_child);
	if (fc_pbuf == InvalidBuffer)
	{
		/*
		 * 未能“重新找到”下链与我们在父层的子块编号匹配的支点元组——索引一定是损坏的。
		 * 甚至不要尝试删除leafbuf子树。只需报告问题并继续清理索引。
		 *
		 * 注意：_bt_getstackbuf()从并发页面拆分中恢复，该拆分发生在父层。其方法是几乎穷举的线性搜索。这也给它在遇到错误或不一致的操作类时意外恢复的机会。但我们在这里不依赖于此。
		 */
		ereport(LOG,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg_internal("failed to re-find parent key in index \"%s\" for deletion target page %u",
								 RelationGetRelationName(fc_rel), fc_child)));
		Assert(false);
		return false;
	}

	fc_parent = fc_stack->bts_blkno;
	fc_parentoffset = fc_stack->bts_offset;

	fc_page = BufferGetPage(fc_pbuf);
	fc_opaque = BTPageGetOpaque(fc_page);
	fc_maxoff = PageGetMaxOffsetNumber(fc_page);
	fc_leftsibparent = fc_opaque->btpo_prev;

	/*
	 * _bt_getstackbuf() 在需要时完成返回的父缓冲区的页面拆分。
	 *
	 * 一般来说，让 VACUUM 使用更多的磁盘空间是个坏主意，这就是为什么页面删除大多数时候不会完成不完整的页面拆分。我们允许这个有限的例外，因为风险要小得多，而不继续的潜在损失要大得多：一个设置了 INCOMPLETE_SPLIT 标志的内部页面，可能会阻止我们从下一级删除数百个空的叶子页面。
	 */
	Assert(!P_INCOMPLETE_SPLIT(fc_opaque));

	if (fc_parentoffset < fc_maxoff)
	{
		/*
		 * 子节点不是父节点中的最右子节点，因此安全删除
		 * 以子页面为根的子树
		 */
		*fc_subtreeparent = fc_pbuf;
		*fc_poffset = fc_parentoffset;
		return true;
	}

	/*
	 * 子节点是父节点的最右子节点。
	 *
	 * 由于它是父节点的最右子节点，因此只有在可以删除父节点时，删除子节点（或删除以子节点页面为根的子树）才是安全的。
	 */
	Assert(fc_parentoffset == fc_maxoff);
	if (fc_parentoffset != P_FIRSTDATAKEY(fc_opaque) || P_RIGHTMOST(fc_opaque))
	{
		/*
		 * 子节点不是父节点唯一的子节点，或者父节点在其
		 * 整个层级上是最右节点。绝对无法删除任何页面。
		 */
		_bt_relbuf(fc_rel, fc_pbuf);
		return false;
	}

	/*
	 * 现在通过检查子节点的祖父页面确保删除父节点本身是安全的。递归，将父页面作为子页面传递（子节点的祖父是上一级的父节点）。如果删除父节点不安全，则删除子节点也一定不安全（在这种情况下，调用者根本无法删除任何页面）。
	 */
	*fc_topparent = fc_parent;
	*fc_topparentrightsib = fc_opaque->btpo_next;

	/*
	 * 在递归之前释放父节点的锁。
	 *
	 * 在递归调用锁定祖父之前，释放父节点的页面锁是可以的。一个内部页面只能在子节点被拆分时才会获取一个条目，但只要我们仍然持有对叶子缓冲页面的锁，这种情况就不会发生。
	 */
	_bt_relbuf(fc_rel, fc_pbuf);

	/*
	 * 在递归之前，检查父节点的左兄弟（如果有）是否未首先标记为 INCOMPLETE_SPLIT 标志（必须在我们释放父节点锁之后执行此操作）。
	 *
	 * 注意：我们故意避免在这里完成不完整的拆分。
	 */
	if (fc__bt_leftsib_splitflag(fc_rel, fc_leftsibparent, fc_parent))
		return false;

	/* 递归检查子页面的祖父页面 */
	return fc__bt_lock_subtree_parent(fc_rel, fc_parent, fc_stack->bts_parent,
								   fc_subtreeparent, fc_poffset,
								   fc_topparent, fc_topparentrightsib);
}

/*
 * 初始化 VACUUM 为 _bt_pendingfsm_finalize 优化使用的本地内存状态。
 *
 * 在 btvacuumscan() 的开始时调用。调用者的 cleanuponly 参数指示正在进行的 VACUUM 是否未（并且不会）调用 btbulkdelete()。
 *
 * 我们预计在这里分配内存到 VACUUM 的顶层内存上下文中。工作缓冲区受限于 work_mem 的限制。当数组在该限制范围内无法再增长时，我们的策略是停止保存额外的新删除页面，同时继续处理我们能够容纳的页面。
 */
void _bt_pendingfsm_init(Relation fc_rel, BTVacState *fc_vstate, bool fc_cleanuponly)
{
	int64		fc_maxbufsize;

	/*
	 * 在仅清理的情况下不必进行优化 -- 我们不期望
	 * 有任何新删除的页面。此外，仅调用 btvacuumscan() 的清理
	 * 只能因为上一次 VACUUM 中优化未成功。
	 */
	if (fc_cleanuponly)
		return;

	/*
	 * 限制数组的最大大小，以便我们始终尊重 work_mem。避免
	 * 整数溢出。
	 */
	fc_vstate->bufsize = 256;
	fc_maxbufsize = (work_mem * 1024L) / sizeof(BTPendingFSM);
	fc_maxbufsize = Min(fc_maxbufsize, INT_MAX);
	fc_maxbufsize = Min(fc_maxbufsize, MaxAllocSize / sizeof(BTPendingFSM));
	/* 保持小的 work_mem 健康 */
	fc_maxbufsize = Max(fc_maxbufsize, fc_vstate->bufsize);
	fc_vstate->maxbufsize = fc_maxbufsize;

	/* 分配缓冲区，表示当前有 0 个待处理页面 */
	fc_vstate->pendingpages = palloc(sizeof(BTPendingFSM) * fc_vstate->bufsize);
	fc_vstate->npendingpages = 0;
}

/*
 * 将任何新删除的页面（即 _bt_pagedel() 在
 * 正在进行的 VACUUM 操作中删除的页面）放入空闲空间映射中——但仅当
 * 现在确实安全时。
 *
 * 在 btvacuumscan() 结束时调用，恰好在空闲空间映射清理
 * 之前。
 *
 * 释放 _bt_pendingfsm_init() 分配的内存（如果有的话）。
 */
void _bt_pendingfsm_finalize(Relation fc_rel, BTVacState *fc_vstate)
{
	IndexBulkDeleteResult *fc_stats = fc_vstate->stats;

	Assert(fc_stats->pages_newly_deleted >= fc_vstate->npendingpages);

	if (fc_vstate->npendingpages == 0)
	{
		/* 当没有要做的事情时仅释放内存 */
		if (fc_vstate->pendingpages)
			pfree(fc_vstate->pendingpages);

		return;
	}

#ifdef DEBUG_BTREE_PENDING_FSM

	/*
	 * 调试辅助：睡眠 5 秒大大提高了将待处理页面放入 FSM 的机会。
	 * 注意，若没有其他后端同时消耗一个
	 * XID，这个优化将永远不起作用。
	 */
	pg_usleep(5000000L);
#endif

	/*
	 * 重新计算 VACUUM XID 边界。
	 *
	 * 我们实际上并不关心最旧的不可移除 XID。计算
	 * 最旧的这种 XID 有一个有用的副作用，我们依赖它：它
	 * 强制更新此后端的 XID 地平线状态。这一步是
	 * 必不可少的；GlobalVisCheckRemovableFullXid() 不会可靠地识别
	 * 在没有这一步的情况下现在安全地回收新删除的页面。
	 */
	GetOldestNonRemovableTransactionId(NULL);

	for (int fc_i = 0; fc_i < fc_vstate->npendingpages; fc_i++)
	{
		BlockNumber fc_target = fc_vstate->pendingpages[fc_i].target;
		FullTransactionId fc_safexid = fc_vstate->pendingpages[fc_i].safexid;

		/*
		 * 做相当于检查 BTPageIsRecyclable() 的事情，但不必
		 * 再次访问页面。
		 *
		 * 放弃查找第一个不可回收页面——所有后面的页面
		 * 也必须是不可回收的，因为 _bt_pendingfsm_add() 按
		 * safexid 顺序将页面添加到数组中。
		 */
		if (!GlobalVisCheckRemovableFullXid(NULL, fc_safexid))
			break;

		RecordFreeIndexPage(fc_rel, fc_target);
		fc_stats->pages_free++;
	}

	pfree(fc_vstate->pendingpages);
}

/*
 * 维护在当前 btvacuumscan() 调用期间删除的页面的数组，
 * 以供 _bt_pendingfsm_finalize() 使用
 */
static void fc__bt_pendingfsm_add(BTVacState *fc_vstate,
				   BlockNumber fc_target,
				   FullTransactionId fc_safexid)
{
	Assert(fc_vstate->npendingpages <= fc_vstate->bufsize);
	Assert(fc_vstate->bufsize <= fc_vstate->maxbufsize);

#ifdef USE_ASSERT_CHECKING

	/*
	 * 验证 _bt_pendingfsm_finalize() 做出的假设：数组中的页面
	 * 将始终按 safexid 顺序（因为这是我们在这里保存它们的顺序）
	 */
	if (fc_vstate->npendingpages > 0)
	{
		FullTransactionId fc_lastsafexid =
		fc_vstate->pendingpages[fc_vstate->npendingpages - 1].safexid;

		Assert(FullTransactionIdFollowsOrEquals(fc_safexid, fc_lastsafexid));
	}
#endif

	/*
	 * 如果临时缓冲区达到 maxbufsize/work_mem 容量，那么我们会丢弃
	 * 关于这个页面的信息。
	 *
	 * 注意，这也涵盖了我们选择不使用
	 * _bt_pendingfsm_init() 中的优化的情况。
	 */
	if (fc_vstate->npendingpages == fc_vstate->maxbufsize)
		return;

	/* 考虑扩大缓冲区 */
	if (fc_vstate->npendingpages == fc_vstate->bufsize)
	{
		int			fc_newbufsize = fc_vstate->bufsize * 2;

		/* 尊重 work_mem */
		if (fc_newbufsize > fc_vstate->maxbufsize)
			fc_newbufsize = fc_vstate->maxbufsize;

		fc_vstate->bufsize = fc_newbufsize;
		fc_vstate->pendingpages =
			repalloc(fc_vstate->pendingpages,
					 sizeof(BTPendingFSM) * fc_vstate->bufsize);
	}

	/* 保存新删除页面的元数据 */
	fc_vstate->pendingpages[fc_vstate->npendingpages].target = fc_target;
	fc_vstate->pendingpages[fc_vstate->npendingpages].safexid = fc_safexid;
	fc_vstate->npendingpages++;
}
